@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
267 lines (257 loc) • 12.8 kB
JavaScript
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
};
};