UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

255 lines (245 loc) 12.1 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 */ const PARENT_WITH_END_DROP_TARGET = ['tableCell', 'tableHeader', 'panel', 'layoutColumn', 'expand', 'nestedExpand', 'bodiedExtension']; const PARENT_WITH_END_DROP_TARGET_SYNC_BLOCK = [...PARENT_WITH_END_DROP_TARGET, 'bodiedSyncBlock']; /** * List of node types that does not allow drop targets at before or after the node. */ const NODE_WITH_NO_PARENT_POS = ['tableCell', 'tableHeader', 'layoutColumn']; const UNSUPPORTED_LAYOUT_CONTENT = ['syncBlock', 'bodiedSyncBlock']; const 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 const canMoveNodeOrSliceToPos = (state, node, parent, index, $toPos, activeNode) => { // For deciding to show drop targets or not when multiple nodes are selected const selection = state.selection; const { $anchor: expandedAnchor, $head: expandedHead } = expandSelectionBounds(selection.$anchor, selection.$head); const selectionFrom = Math.min(expandedAnchor.pos, expandedHead.pos); const selectionTo = Math.max(expandedAnchor.pos, expandedHead.pos); const activeNodePos = activeNode === null || activeNode === void 0 ? void 0 : activeNode.pos; const $activeNodePos = typeof activeNodePos === 'number' && state.doc.resolve(activeNodePos); const activePMNode = $activeNodePos && $activeNodePos.nodeAfter; const handleInsideSelection = activeNodePos !== undefined && activeNodePos >= selectionFrom && activeNodePos <= selectionTo; if (editorExperiment('platform_editor_element_drag_and_drop_multiselect', true)) { const selectionSlice = state.doc.slice(selectionFrom, selectionTo, false); const selectionSliceChildCount = selectionSlice.content.childCount; let canDropSingleNode = true; let 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 { const canDrop = activePMNode && canMoveNodeToIndex(parent, index, activePMNode, $toPos, node); return canDrop; } return true; }; export const getActiveDropTargetDecorations = (activeDropTargetNode, state, api, existingDecs, formatMessage, nodeViewPortalProviderAPI, activeNode, anchorRectCache) => { const decsToAdd = []; let decsToRemove = existingDecs.filter(dec => !!dec); const activeNodePos = activeNode === null || activeNode === void 0 ? void 0 : activeNode.pos; const $activeNodePos = typeof activeNodePos === 'number' && state.doc.resolve(activeNodePos); const $toPos = state.doc.resolve(activeDropTargetNode.pos); const existingDecsPos = decsToRemove.map(dec => dec.from); const { parent, index, node, pos, before, after, depth } = findSurroundingNodes(state, $toPos, activeDropTargetNode.nodeTypeName); /** * 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)) { const isEmptyContainer = node.childCount === 0 || node.childCount === 1 && isEmptyParagraph(node.firstChild); // can move to before first child const 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(dec => dec.from !== posBeforeFirstChild); } else { decsToAdd.push(createDropTargetDecoration(posBeforeFirstChild, { api, prevNode: undefined, nextNode: node.firstChild, parentNode: node, 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) { const 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(dec => dec.from !== posAfterLastChild); } else { decsToAdd.push(createDropTargetDecoration(posAfterLastChild, { api, prevNode: node.lastChild, nextNode: undefined, parentNode: node, 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)) { const isSameLayout = $activeNodePos && isInSameLayout($activeNodePos, $toPos); const isInSupportedContainer = ['tableCell', 'tableHeader', 'layoutColumn'].includes((parent === null || parent === void 0 ? void 0 : parent.type.name) || ''); const 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(dec => dec.from !== pos); } else { decsToAdd.push(createDropTargetDecoration(pos, { api, prevNode: before || undefined, nextNode: node, parentNode: parent || undefined, formatMessage, dropTargetStyle: shouldShowFullHeight ? 'remainingHeight' : 'default' }, nodeViewPortalProviderAPI, -1, anchorRectCache, isSameLayout)); } } // if the node is a container node, we show the drop target after the node const 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(dec => dec.from !== posAfterNode); } else { decsToAdd.push(createDropTargetDecoration(posAfterNode, { api, prevNode: node, nextNode: after || undefined, parentNode: parent || undefined, formatMessage, dropTargetStyle: shouldShowFullHeight ? 'remainingHeight' : 'default' }, nodeViewPortalProviderAPI, -1, anchorRectCache, isSameLayout)); } } } let rootNodeWithPos = { node, pos }; // if the current node is not a top level node, we create one for advanced layout drop targets if (depth > 1) { const 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(dec => dec.from !== root.pos); } else { decsToAdd.push(createDropTargetDecoration(root.pos, { api, prevNode: root.before || undefined, nextNode: root.node, parentNode: state.doc || undefined, formatMessage, dropTargetStyle: 'default' }, nodeViewPortalProviderAPI, -1, anchorRectCache, false)); } rootNodeWithPos = { node: root.node, pos: root.pos }; } let anchorEmitNodeWithPos = rootNodeWithPos; if (editorExperiment('advanced_layouts', true)) { if (editorExperiment('platform_synced_block', true) && editorExperiment('platform_synced_block_patch_6', true, { exposure: true })) { const schema = rootNodeWithPos.node.type.schema; const { layoutSection } = schema.nodes; const isLayoutSectionChildOfRoot = findChildrenByType(rootNodeWithPos.node, layoutSection, false).length > 0; if (isLayoutSectionChildOfRoot) { // if node has layoutSection as a child, get the layoutSection node and pos for (let 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; } const isSameLayout = $activeNodePos && isInSameLayout($activeNodePos, state.doc.resolve(anchorEmitNodeWithPos.pos)); const 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) { const layoutSectionNode = anchorEmitNodeWithPos.node; if (layoutSectionNode.childCount < maxLayoutColumnSupported() || isSameLayout) { layoutSectionNode.descendants((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 ) { const currentPos = anchorEmitNodeWithPos.pos + childPos + 1; if (existingDecsPos.includes(currentPos)) { // if the decoration already exists, we don't add it again. decsToRemove = decsToRemove.filter(dec => dec.from !== currentPos); } else { decsToAdd.push(createLayoutDropTargetDecoration(anchorEmitNodeWithPos.pos + childPos + 1, { api, parent, 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, decsToRemove }; };