UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

267 lines (257 loc) 12.8 kB
import { expandSelectionBounds } from '@atlaskit/editor-common/selection'; import { isEmptyParagraph } from '@atlaskit/editor-common/utils'; import { findChildrenByType } from '@atlaskit/editor-prosemirror/utils'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { getNodeAnchor } from './decorations-common'; import { createDropTargetDecoration, createLayoutDropTargetDecoration } from './decorations-drop-target'; import { findSurroundingNodes } from './decorations-find-surrounding-nodes'; import { defaultActiveAnchorTracker } from './utils/active-anchor-tracker'; import { maxLayoutColumnSupported } from './utils/consts'; import { canMoveNodeToIndex, canMoveSliceToIndex, isInSameLayout } from './utils/validation'; /** * List of parent node types that can have child nodes */ var PARENT_WITH_END_DROP_TARGET = ['tableCell', 'tableHeader', 'panel', 'layoutColumn', 'expand', 'nestedExpand', 'bodiedExtension']; var PARENT_WITH_END_DROP_TARGET_SYNC_BLOCK = [].concat(PARENT_WITH_END_DROP_TARGET, ['bodiedSyncBlock']); /** * List of node types that does not allow drop targets at before or after the node. */ var NODE_WITH_NO_PARENT_POS = ['tableCell', 'tableHeader', 'layoutColumn']; var UNSUPPORTED_LAYOUT_CONTENT = ['syncBlock', 'bodiedSyncBlock']; var isContainerNode = function isContainerNode(node) { if (editorExperiment('platform_synced_block', true)) { return PARENT_WITH_END_DROP_TARGET_SYNC_BLOCK.includes(node.type.name); } return PARENT_WITH_END_DROP_TARGET.includes(node.type.name); }; export var canMoveNodeOrSliceToPos = function canMoveNodeOrSliceToPos(state, node, parent, index, $toPos, activeNode) { // For deciding to show drop targets or not when multiple nodes are selected var selection = state.selection; var _expandSelectionBound = expandSelectionBounds(selection.$anchor, selection.$head), expandedAnchor = _expandSelectionBound.$anchor, expandedHead = _expandSelectionBound.$head; var selectionFrom = Math.min(expandedAnchor.pos, expandedHead.pos); var selectionTo = Math.max(expandedAnchor.pos, expandedHead.pos); var activeNodePos = activeNode === null || activeNode === void 0 ? void 0 : activeNode.pos; var $activeNodePos = typeof activeNodePos === 'number' && state.doc.resolve(activeNodePos); var activePMNode = $activeNodePos && $activeNodePos.nodeAfter; var handleInsideSelection = activeNodePos !== undefined && activeNodePos >= selectionFrom && activeNodePos <= selectionTo; if (editorExperiment('platform_editor_element_drag_and_drop_multiselect', true)) { var selectionSlice = state.doc.slice(selectionFrom, selectionTo, false); var selectionSliceChildCount = selectionSlice.content.childCount; var canDropSingleNode = true; var canDropMultipleNodes = true; // when there is only one node in the slice, use the same logic as when multi select is not on if (selectionSliceChildCount > 1 && handleInsideSelection) { canDropMultipleNodes = canMoveSliceToIndex(selectionSlice, selectionFrom, selectionTo, parent, index, $toPos); } else { canDropSingleNode = !!(activePMNode && canMoveNodeToIndex(parent, index, activePMNode, $toPos, node)); } if (!canDropMultipleNodes || !canDropSingleNode) { return false; } } else { var canDrop = activePMNode && canMoveNodeToIndex(parent, index, activePMNode, $toPos, node); return canDrop; } return true; }; export var getActiveDropTargetDecorations = function getActiveDropTargetDecorations(activeDropTargetNode, state, api, existingDecs, formatMessage, nodeViewPortalProviderAPI, activeNode, anchorRectCache) { var decsToAdd = []; var decsToRemove = existingDecs.filter(function (dec) { return !!dec; }); var activeNodePos = activeNode === null || activeNode === void 0 ? void 0 : activeNode.pos; var $activeNodePos = typeof activeNodePos === 'number' && state.doc.resolve(activeNodePos); var $toPos = state.doc.resolve(activeDropTargetNode.pos); var existingDecsPos = decsToRemove.map(function (dec) { return dec.from; }); var _findSurroundingNodes = findSurroundingNodes(state, $toPos, activeDropTargetNode.nodeTypeName), parent = _findSurroundingNodes.parent, index = _findSurroundingNodes.index, node = _findSurroundingNodes.node, pos = _findSurroundingNodes.pos, before = _findSurroundingNodes.before, after = _findSurroundingNodes.after, depth = _findSurroundingNodes.depth; /** * If the current node is a container node, we show the drop targets * above the first child and below the last child. */ if (isContainerNode(node)) { var isEmptyContainer = node.childCount === 0 || node.childCount === 1 && isEmptyParagraph(node.firstChild); // can move to before first child var posBeforeFirstChild = pos + 1; // +1 to get the position of the first child if (node.firstChild && canMoveNodeOrSliceToPos(state, node.firstChild, node, 0, state.doc.resolve(posBeforeFirstChild), activeNode)) { if (existingDecsPos.includes(posBeforeFirstChild)) { // if the decoration already exists, we don't add it again. decsToRemove = decsToRemove.filter(function (dec) { return dec.from !== posBeforeFirstChild; }); } else { decsToAdd.push(createDropTargetDecoration(posBeforeFirstChild, { api: api, prevNode: undefined, nextNode: node.firstChild, parentNode: node, formatMessage: formatMessage, dropTargetStyle: isEmptyContainer ? 'remainingHeight' : 'default' }, nodeViewPortalProviderAPI, 1, anchorRectCache)); } } // can move to after last child // if the node is empty, we don't show the drop target at the end of the node if (!isEmptyContainer) { var posAfterLastChild = pos + node.nodeSize - 1; // -1 to get the position after last child if (node.lastChild && canMoveNodeOrSliceToPos(state, node.lastChild, node, node.childCount - 1, state.doc.resolve(posAfterLastChild), // -1 to get the position after last child activeNode)) { if (existingDecsPos.includes(posAfterLastChild)) { // if the decoration already exists, we don't add it again. decsToRemove = decsToRemove.filter(function (dec) { return dec.from !== posAfterLastChild; }); } else { decsToAdd.push(createDropTargetDecoration(posAfterLastChild, { api: api, prevNode: node.lastChild, nextNode: undefined, parentNode: node, formatMessage: formatMessage, dropTargetStyle: 'remainingHeight' }, nodeViewPortalProviderAPI, -1, anchorRectCache)); } } } } /** * Create drop target before and after the current node */ if (!NODE_WITH_NO_PARENT_POS.includes(node.type.name)) { var isSameLayout = $activeNodePos && isInSameLayout($activeNodePos, $toPos); var isInSupportedContainer = ['tableCell', 'tableHeader', 'layoutColumn'].includes((parent === null || parent === void 0 ? void 0 : parent.type.name) || ''); var shouldShowFullHeight = isInSupportedContainer && (parent === null || parent === void 0 ? void 0 : parent.lastChild) === node && isEmptyParagraph(node); if (canMoveNodeOrSliceToPos(state, node, parent, index, $toPos, activeNode)) { if (existingDecsPos.includes(pos)) { // if the decoration already exists, we don't add it again. decsToRemove = decsToRemove.filter(function (dec) { return dec.from !== pos; }); } else { decsToAdd.push(createDropTargetDecoration(pos, { api: api, prevNode: before || undefined, nextNode: node, parentNode: parent || undefined, formatMessage: formatMessage, dropTargetStyle: shouldShowFullHeight ? 'remainingHeight' : 'default' }, nodeViewPortalProviderAPI, -1, anchorRectCache, isSameLayout)); } } // if the node is a container node, we show the drop target after the node var posAfterNode = pos + node.nodeSize; if (canMoveNodeOrSliceToPos(state, node, parent, index + 1, state.doc.resolve(posAfterNode), activeNode)) { if (existingDecsPos.includes(posAfterNode)) { // if the decoration already exists, we don't add it again. decsToRemove = decsToRemove.filter(function (dec) { return dec.from !== posAfterNode; }); } else { decsToAdd.push(createDropTargetDecoration(posAfterNode, { api: api, prevNode: node, nextNode: after || undefined, parentNode: parent || undefined, formatMessage: formatMessage, dropTargetStyle: shouldShowFullHeight ? 'remainingHeight' : 'default' }, nodeViewPortalProviderAPI, -1, anchorRectCache, isSameLayout)); } } } var rootNodeWithPos = { node: node, pos: pos }; // if the current node is not a top level node, we create one for advanced layout drop targets if (depth > 1) { var root = findSurroundingNodes(state, state.doc.resolve($toPos.before(2))); if (existingDecsPos.includes(root.pos)) { // if the decoration already exists, we don't add it again. decsToRemove = decsToRemove.filter(function (dec) { return dec.from !== root.pos; }); } else { decsToAdd.push(createDropTargetDecoration(root.pos, { api: api, prevNode: root.before || undefined, nextNode: root.node, parentNode: state.doc || undefined, formatMessage: formatMessage, dropTargetStyle: 'default' }, nodeViewPortalProviderAPI, -1, anchorRectCache, false)); } rootNodeWithPos = { node: root.node, pos: root.pos }; } var anchorEmitNodeWithPos = rootNodeWithPos; if (editorExperiment('advanced_layouts', true)) { if (editorExperiment('platform_synced_block', true) && editorExperiment('platform_synced_block_patch_6', true, { exposure: true })) { var schema = rootNodeWithPos.node.type.schema; var layoutSection = schema.nodes.layoutSection; var isLayoutSectionChildOfRoot = findChildrenByType(rootNodeWithPos.node, layoutSection, false).length > 0; if (isLayoutSectionChildOfRoot) { // if node has layoutSection as a child, get the layoutSection node and pos for (var ancestorDepth = $toPos.depth; ancestorDepth >= 1; ancestorDepth--) { if ($toPos.node(ancestorDepth).type.name === 'layoutSection') { anchorEmitNodeWithPos = { node: $toPos.node(ancestorDepth), pos: $toPos.before(ancestorDepth) }; break; } } } else { anchorEmitNodeWithPos = rootNodeWithPos; } } else { anchorEmitNodeWithPos = rootNodeWithPos; } var _isSameLayout = $activeNodePos && isInSameLayout($activeNodePos, state.doc.resolve(anchorEmitNodeWithPos.pos)); var hasUnsupportedContent = UNSUPPORTED_LAYOUT_CONTENT.includes((activeNode === null || activeNode === void 0 ? void 0 : activeNode.nodeType) || '') && editorExperiment('platform_synced_block', true); if (anchorEmitNodeWithPos.node.type.name === 'layoutSection' && !hasUnsupportedContent) { var layoutSectionNode = anchorEmitNodeWithPos.node; if (layoutSectionNode.childCount < maxLayoutColumnSupported() || _isSameLayout) { layoutSectionNode.descendants(function (childNode, childPos, parent, index) { if (childNode.type.name === 'layoutColumn' && (parent === null || parent === void 0 ? void 0 : parent.type.name) === 'layoutSection' && index !== 0 // Not the first node ) { var currentPos = anchorEmitNodeWithPos.pos + childPos + 1; if (existingDecsPos.includes(currentPos)) { // if the decoration already exists, we don't add it again. decsToRemove = decsToRemove.filter(function (dec) { return dec.from !== currentPos; }); } else { decsToAdd.push(createLayoutDropTargetDecoration(anchorEmitNodeWithPos.pos + childPos + 1, { api: api, parent: parent, formatMessage: formatMessage }, nodeViewPortalProviderAPI, anchorRectCache)); } } return false; }); } } } defaultActiveAnchorTracker.emit(expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) ? api.core.actions.getAnchorIdForNode(anchorEmitNodeWithPos.node, anchorEmitNodeWithPos.pos) || '' : getNodeAnchor(anchorEmitNodeWithPos.node)); return { decsToAdd: decsToAdd, decsToRemove: decsToRemove }; };