UNPKG

@atlaskit/editor-plugin-block-menu

Version:

BlockMenu plugin for @atlaskit/editor-core

195 lines (194 loc) • 9.78 kB
import { expandedState } from '@atlaskit/editor-common/expand'; import { startMeasure, stopMeasure } from '@atlaskit/editor-common/performance-measures'; import { expandSelectionToBlockRange, getSourceNodesFromSelectionRange } from '@atlaskit/editor-common/selection/utils'; import { Fragment, Node } from '@atlaskit/editor-prosemirror/model'; import { NodeSelection, Selection } from '@atlaskit/editor-prosemirror/state'; import { Mapping, StepMap } from '@atlaskit/editor-prosemirror/transform'; import { CellSelection } from '@atlaskit/editor-tables/cell-selection'; import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled'; import { getSingleTransformSourceNode, isExtensionTransformSource } from '../editor-actions/transformSource'; import { isNestedNode } from '../ui/utils/isNestedNode'; import { isEmptyLine } from './is-empty-line'; import { createTransformAnalytics } from './transform-analytics'; import { convertNodesToTargetType } from './transform-node-utils/transform'; import { isListNode } from './transform-node-utils/utils'; export const transformNode = (api, transformRegistry) => (targetType, metadata) => ({ tr }) => { var _api$blockControls, _api$blockControls$sh; const preservedSelection = api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : (_api$blockControls$sh = _api$blockControls.sharedState.currentState()) === null || _api$blockControls$sh === void 0 ? void 0 : _api$blockControls$sh.preservedSelection; if (!preservedSelection) { return tr; } const isSmallTextTransform = (metadata === null || metadata === void 0 ? void 0 : metadata.targetTypeName) === 'smallText'; const isTextFormattingTransform = ((metadata === null || metadata === void 0 ? void 0 : metadata.marksToAdd) !== undefined || (metadata === null || metadata === void 0 ? void 0 : metadata.marksToRemove) !== undefined) && isExperimentEnabled('platform_editor_block_menu_small_text'); if (isSmallTextTransform && !isTextFormattingTransform) { return null; } const analytics = createTransformAnalytics(api, tr, preservedSelection); const { $from, $to, range } = expandSelectionToBlockRange(preservedSelection); const sourceNode = getSingleTransformSourceNode(preservedSelection, range); if (isExtensionTransformSource(sourceNode)) { var _metadata$targetTypeN; const context = { source: sourceNode.toJSON(), targetTypeName: (_metadata$targetTypeN = metadata === null || metadata === void 0 ? void 0 : metadata.targetTypeName) !== null && _metadata$targetTypeN !== void 0 ? _metadata$targetTypeN : targetType.name }; const resolution = transformRegistry === null || transformRegistry === void 0 ? void 0 : transformRegistry.resolve(context); if (!resolution || resolution.status === 'unsupported') { return tr; } const measureId = `transformNode_${targetType.name}_${Date.now()}`; startMeasure(measureId); try { var _api$blockControls2, _api$blockControls3; const transformResult = resolution.transform.transform(context); if (!transformResult) { stopMeasure(measureId); return tr; } if (!Array.isArray(transformResult.output) || transformResult.output.length === 0) { throw new Error('Block menu transform returned no output'); } const resultNodes = transformResult.output.map(nodeAdf => { const node = Node.fromJSON(tr.doc.type.schema, nodeAdf); node.check(); return node; }); if (!range || !range.parent.canReplace(range.startIndex, range.endIndex, Fragment.from(resultNodes))) { throw new Error('Block menu transform output is invalid at the selected position'); } const sliceStart = $from.pos; tr.replaceWith(sliceStart, $to.pos, resultNodes); const insertedNode = tr.doc.nodeAt(sliceStart); const nextSelection = insertedNode && NodeSelection.isSelectable(insertedNode) ? NodeSelection.create(tr.doc, sliceStart) : Selection.near(tr.doc.resolve(sliceStart)); tr.setSelection(nextSelection); api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : _api$blockControls2.commands.stopPreservingSelection()({ tr }); api === null || api === void 0 ? void 0 : (_api$blockControls3 = api.blockControls) === null || _api$blockControls3 === void 0 ? void 0 : _api$blockControls3.commands.toggleBlockMenu({ closeMenu: true })({ tr }); const { expand, nestedExpand } = tr.doc.type.schema.nodes; resultNodes.forEach(node => { if (node.type === expand || node.type === nestedExpand) { expandedState.set(node, true); } }); stopMeasure(measureId, (duration, startTime) => { analytics.transformed(duration, startTime, { isNested: isNestedNode(preservedSelection, ''), isSuggested: Boolean(metadata === null || metadata === void 0 ? void 0 : metadata.isSuggested), outputNodesCount: resultNodes.length, sourceNodes: [sourceNode], targetNodeType: targetType.name }); }); } catch (error) { stopMeasure(measureId); analytics.errored(error, [sourceNode], targetType.name); } return tr; } const measureId = `transformNode_${targetType.name}_${Date.now()}`; startMeasure(measureId); const docBeforeTransform = tr.doc; const { nodes } = tr.doc.type.schema; const selectedParent = $from.parent; const isParentLayout = selectedParent.type === nodes.layoutColumn; const isNested = isNestedNode(preservedSelection, '') && !isParentLayout; const isList = isListNode(selectedParent); const sourceNodes = getSourceNodesFromSelectionRange(tr, preservedSelection); const isSuggested = Boolean(metadata === null || metadata === void 0 ? void 0 : metadata.isSuggested); try { var _api$blockControls6; const resultNodes = convertNodesToTargetType({ sourceNodes, targetNodeType: targetType, schema: tr.doc.type.schema, isNested, targetAttrs: metadata === null || metadata === void 0 ? void 0 : metadata.targetAttrs, marksToAdd: isTextFormattingTransform ? metadata === null || metadata === void 0 ? void 0 : metadata.marksToAdd : undefined, marksToRemove: isTextFormattingTransform ? metadata === null || metadata === void 0 ? void 0 : metadata.marksToRemove : undefined, parentNode: selectedParent }); const content = resultNodes.length > 0 ? resultNodes : sourceNodes; const sliceStart = isList ? $from.pos - 1 : $from.pos; const { expand, nestedExpand } = nodes; content.forEach(node => { if (node.type === expand || node.type === nestedExpand) { expandedState.set(node, true); } }); if (preservedSelection instanceof NodeSelection && preservedSelection.node.type === nodes.mediaSingle) { var _api$blockControls4; // when node is media single, use tr.replaceWith freeze editor, if modify position, tr.replaceWith creates duplicats const deleteFrom = $from.pos; const deleteTo = $to.pos; tr.delete(deleteFrom, deleteTo); // After deletion, recalculate the insertion position to ensure it's valid // especially when mediaSingle with caption is at the bottom of the document const insertPos = Math.min(deleteFrom, tr.doc.content.size); tr.insert(insertPos, content); // when we replace and insert content, we need to manually map the preserved selection // through the transaction, otherwise it will treat the selection as having been deleted // and stop preserving it const oldSize = sourceNodes.reduce((sum, node) => sum + node.nodeSize, 0); const newSize = content.reduce((sum, node) => sum + node.nodeSize, 0); api === null || api === void 0 ? void 0 : (_api$blockControls4 = api.blockControls) === null || _api$blockControls4 === void 0 ? void 0 : _api$blockControls4.commands.mapPreservedSelection(new Mapping([new StepMap([0, oldSize, newSize])]))({ tr }); } else { tr.replaceWith(sliceStart, $to.pos, content); } if (isTextFormattingTransform && tr.doc.eq(docBeforeTransform)) { stopMeasure(measureId); return null; } if (preservedSelection instanceof CellSelection) { const insertedNode = tr.doc.nodeAt($from.pos); const isSelectable = insertedNode && NodeSelection.isSelectable(insertedNode); if (isSelectable) { var _api$blockControls5; const nodeSelection = NodeSelection.create(tr.doc, $from.pos); tr.setSelection(nodeSelection); api === null || api === void 0 ? void 0 : (_api$blockControls5 = api.blockControls) === null || _api$blockControls5 === void 0 ? void 0 : _api$blockControls5.commands.startPreservingSelection()({ tr }); } } api === null || api === void 0 ? void 0 : (_api$blockControls6 = api.blockControls) === null || _api$blockControls6 === void 0 ? void 0 : _api$blockControls6.commands.toggleBlockMenu({ closeMenu: true })({ tr }); stopMeasure(measureId, (duration, startTime) => { analytics.transformed(duration, startTime, { isEmptyLine: isEmptyLine(sourceNodes), isNested, isSuggested, outputNodesCount: content.length, sourceNodes, targetNodeType: isSmallTextTransform ? 'smallText' : targetType.name }); }); } catch (error) { stopMeasure(measureId); analytics.errored(error, sourceNodes, targetType.name); } return tr; };