@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
255 lines (245 loc) • 12.1 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
*/
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
};
};