@atlaskit/editor-plugin-block-menu
Version:
BlockMenu plugin for @atlaskit/editor-core
216 lines (210 loc) • 10.8 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.wrapMixedContentStep = void 0;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _codeBlock = require("@atlaskit/editor-common/code-block");
var _utils = require("@atlaskit/editor-common/utils");
var _model = require("@atlaskit/editor-prosemirror/model");
var _marks = require("../marks");
var _types = require("../types");
var _utils2 = require("../utils");
/**
* Creates a layout section with two columns, where the first column contains the provided content.
* Preserves breakout marks if provided.
*/
var createLayoutSection = function createLayoutSection(content, layoutSection, layoutColumn, marks) {
var columnOne = layoutColumn.createAndFill({}, (0, _marks.removeDisallowedMarks)(content, layoutColumn));
var columnTwo = layoutColumn.createAndFill();
if (!columnOne || !columnTwo) {
return null;
}
return layoutSection.createAndFill({}, [columnOne, columnTwo], marks);
};
/**
* Creates a container with text content (for codeblocks).
*/
var createTextContentContainer = function createTextContentContainer(textContentArray, targetNodeType, schema) {
var textContent = textContentArray.join('\n');
var textNode = textContent ? schema.text(textContent) : null;
var attrs = targetNodeType.name === 'codeBlock' ? (0, _codeBlock.getDefaultCodeBlockAttrs)() : {};
return targetNodeType.createAndFill(attrs, textNode);
};
/**
* Creates a regular container with node content.
*/
var createNodeContentContainer = function createNodeContentContainer(nodeContent, targetNodeType) {
var isExpandType = targetNodeType.name === 'expand' || targetNodeType.name === 'nestedExpand';
var nodeAttrs = isExpandType ? {
localId: crypto.randomUUID()
} : {};
return targetNodeType.createAndFill(nodeAttrs, nodeContent);
};
/**
* Handles the edge case where transforming from a container to another container results in
* all content breaking out (no valid children for the target). In this case, creates an empty
* container to ensure the target container type is created.
*
* We can determine if there were no valid children by checking if no container was created
* (`!hasCreatedContainer`) and there are nodes in the result (`result.length > 0`), which
* means all content broke out rather than being wrapped.
*
* @param result - The current result nodes after processing
* @param hasCreatedContainer - Whether a container was already created during processing
* @param fromNode - The original source node (before unwrapping)
* @param targetNodeType - The target container type
* @param targetNodeTypeName - The target container type name
* @param schema - The schema
* @returns The result nodes with an empty container prepended if needed, or the original result
*/
var handleEmptyContainerEdgeCase = function handleEmptyContainerEdgeCase(result, hasCreatedContainer, fromNode, targetNodeType, targetNodeTypeName, schema) {
var isFromContainer = _types.NODE_CATEGORY_BY_TYPE[fromNode.type.name] === 'container';
var isTargetContainer = _types.NODE_CATEGORY_BY_TYPE[targetNodeTypeName] === 'container';
// If no container was created but we have nodes in result, all content broke out
// (meaning there were no valid children that could be wrapped)
var allContentBrokeOut = !hasCreatedContainer && result.length > 0;
var shouldCreateEmptyTarget = isFromContainer && isTargetContainer && allContentBrokeOut;
if (!shouldCreateEmptyTarget) {
return result;
}
if (targetNodeTypeName === schema.nodes.codeBlock.name) {
var emptyCodeBlock = createTextContentContainer([], schema.nodes.codeBlock, schema);
return emptyCodeBlock ? [emptyCodeBlock].concat((0, _toConsumableArray2.default)(result)) : result;
}
var emptyParagraph = schema.nodes.paragraph.create();
var isExpandType = targetNodeTypeName === 'expand' || targetNodeTypeName === 'nestedExpand';
var emptyContainerAttrs = isExpandType ? {
localId: crypto.randomUUID()
} : {};
var emptyContainer = targetNodeType.create(emptyContainerAttrs, emptyParagraph);
return [emptyContainer].concat((0, _toConsumableArray2.default)(result));
};
/**
* A wrap step that handles mixed content according to the Compatibility Matrix:
* - Wraps consecutive compatible nodes into the target container
* - Same-type containers break out as separate containers (preserved as-is)
* - NestedExpands break out as regular expands (converted since nestedExpand can't exist outside expand)
* - Container structures that can't be nested in target break out (not flattened)
* - Text/list nodes that can't be wrapped are converted to paragraphs and merged into the container
* - Atomic nodes (tables, media, macros) break out
*
* Special handling for layouts:
* - Layout sections break out as separate layouts (preserved as-is, not wrapped)
* - Other nodes (including headings, paragraphs, lists) are wrapped into layout columns within a layout section
* - Layouts always require layoutColumns as children (never paragraphs directly)
* - Layout columns can contain most block content including headings, paragraphs, lists, etc.
*
* Special handling for codeblocks:
* - Text nodes are converted to plain text and added to the codeblock
*
* Edge case handling:
* - For regular containers: If all content breaks out (container → container transform with no
* valid children), an empty container with a paragraph is created to ensure the target type exists
* - For layouts: Edge case handling is skipped because layouts require columns, not direct paragraphs.
* If all content breaks out, only the broken-out nodes are returned (no empty layout created)
*
* What can be wrapped depends on the target container's schema:
* - expand → panel: tables break out, nestedExpands convert to expands and break out
* - expand → blockquote: tables/media break out, nestedExpands convert to expands and break out, headings converted to paragraphs
* - expand → expand: tables/media stay inside (expands can contain them)
* - multi → layoutSection: layout sections break out, headings/paragraphs/lists wrapped into layout columns
*
* Example: expand(p('a'), table(), p('b')) → panel: [panel(p('a')), table(), panel(p('b'))]
* Example: expand(p('a'), panel(p('x')), p('b')) → panel: [panel(p('a')), panel(p('x')), panel(p('b'))]
* Example: expand(p('a'), nestedExpand({title: 'inner'})(p('x')), p('b')) → panel: [panel(p('a')), expand({title: 'inner'})(p('x')), panel(p('b'))]
* Example: expand(nestedExpand()(p())) → panel: [panel(), expand()(p())] (empty panel when all content breaks out)
* Example: [p('a'), layoutSection(...), p('b')] → layoutSection: [layoutSection(layoutColumn(p('a'))), layoutSection(...), layoutSection(layoutColumn(p('b')))]
* Example: [h1('heading'), p('para')] → layoutSection: [layoutSection(layoutColumn(h1('heading'), p('para')))] (headings stay as headings in layouts)
*/
var wrapMixedContentStep = exports.wrapMixedContentStep = function wrapMixedContentStep(nodes, context) {
var schema = context.schema,
targetNodeTypeName = context.targetNodeTypeName,
fromNode = context.fromNode;
var targetNodeType = schema.nodes[targetNodeTypeName];
if (!targetNodeType) {
return nodes;
}
var isLayout = targetNodeTypeName === 'layoutSection';
var isCodeblock = targetNodeTypeName === 'codeBlock';
var _schema$nodes = schema.nodes,
layoutSection = _schema$nodes.layoutSection,
layoutColumn = _schema$nodes.layoutColumn;
var sourceSupportsBreakout = _utils.breakoutResizableNodes.includes(fromNode.type.name);
var targetSupportsBreakout = _utils.breakoutResizableNodes.includes(targetNodeTypeName);
var shouldPreserveBreakout = sourceSupportsBreakout && targetSupportsBreakout;
var breakoutMark;
if (shouldPreserveBreakout) {
breakoutMark = fromNode.marks.find(function (mark) {
return mark.type.name === 'breakout';
});
}
var result = [];
var currentContainerContent = [];
var hasCreatedContainer = false;
var flushCurrentContainer = function flushCurrentContainer() {
if (currentContainerContent.length === 0) {
return;
}
var container = null;
if (isLayout) {
container = createLayoutSection(currentContainerContent, layoutSection, layoutColumn, breakoutMark ? [breakoutMark] : undefined);
} else if (isCodeblock) {
container = createTextContentContainer(currentContainerContent, targetNodeType, schema);
} else {
container = createNodeContentContainer(currentContainerContent, targetNodeType);
}
if (container) {
result.push(container);
hasCreatedContainer = true;
}
currentContainerContent = [];
};
var canNodeBeWrapped = function canNodeBeWrapped(node) {
var validationType = isLayout ? layoutColumn : targetNodeType;
return validationType.validContent(_model.Fragment.from((0, _marks.removeDisallowedMarks)([node], validationType)));
};
var handleWrappableNode = function handleWrappableNode(node) {
var _currentContainerCont;
var validationType = isLayout ? layoutColumn : targetNodeType;
(_currentContainerCont = currentContainerContent).push.apply(_currentContainerCont, (0, _toConsumableArray2.default)((0, _marks.removeDisallowedMarks)([node], validationType)));
};
var handleCodeblockTextNode = function handleCodeblockTextNode(node) {
currentContainerContent.push((0, _utils2.createTextContent)(node));
};
var handleConvertibleTextNode = function handleConvertibleTextNode(node) {
var paragraph = (0, _utils2.convertTextNodeToParagraph)(node, schema);
if (paragraph) {
currentContainerContent.push(paragraph);
}
};
var handleUnsupportedNode = function handleUnsupportedNode(node) {
flushCurrentContainer();
result.push(node);
};
var processNode = function processNode(node) {
if (canNodeBeWrapped(node)) {
handleWrappableNode(node);
return;
}
if ((0, _utils2.isTextNode)(node) && isCodeblock) {
handleCodeblockTextNode(node);
return;
}
if ((0, _utils2.isTextNode)(node)) {
handleConvertibleTextNode(node);
return;
}
// All other nodes that cannot be wrapped in the target node - break out
// Examples: same-type containers, tables in panels, layoutSections in layouts
handleUnsupportedNode(node);
};
nodes.forEach(processNode);
flushCurrentContainer();
if (isLayout) {
return result.length > 0 ? result : nodes;
}
var finalResult = handleEmptyContainerEdgeCase(result, hasCreatedContainer, fromNode, targetNodeType, targetNodeTypeName, schema);
return finalResult.length > 0 ? finalResult : nodes;
};