UNPKG

@atlaskit/editor-plugin-block-menu

Version:

BlockMenu plugin for @atlaskit/editor-core

216 lines (210 loc) 10.8 kB
"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; };