@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
287 lines (285 loc) • 14.2 kB
JavaScript
/* eslint-disable @atlaskit/design-system/consistent-css-prop-usage */
/* eslint-disable @atlaskit/ui-styling-standard/no-unsafe-values */
/**
* @jsxRuntime classic
* @jsx jsx
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports
import { css, jsx } from '@emotion/react';
import { akEditorBreakoutPadding } from '@atlaskit/editor-shared-styles';
import { DropIndicator } from '@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box';
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { getNodeAnchor } from '../pm-plugins/decorations-common';
import { useActiveAnchorTracker } from '../pm-plugins/utils/active-anchor-tracker';
import { isAnchorSupported } from '../pm-plugins/utils/anchor-utils';
import { getInsertLayoutStep, updateSelection } from '../pm-plugins/utils/update-selection';
const HOVER_ZONE_WIDTH = '--editor-blocks-inline-hover-zone-width';
const HOVER_ZONE_HEIGHT = '--editor-blocks-inline-hover-zone-height';
const HOVER_ZONE_TOP = '--editor-blocks-inline-hover-zone-top';
const HOVER_ZONE_BOTTOM = '--editor-blocks-inline-hover-zone-bottom';
const HOVER_ZONE_ANCHOR_NAME = '--editor-blocks-inline-hover-zone-anchor-name';
const hoverZoneCommonStyle = css({
position: 'absolute',
// above the top and bottom drop zone as block hover zone
zIndex: 120,
positionAnchor: `var(${HOVER_ZONE_ANCHOR_NAME})`,
minWidth: "var(--ds-space-100, 8px)",
left: 0,
right: 0,
width: `var(${HOVER_ZONE_WIDTH})`,
height: `var(${HOVER_ZONE_HEIGHT})`
});
const leftHoverZoneStyle = css({
right: `unset`,
top: `var(${HOVER_ZONE_TOP})`,
bottom: 'unset'
});
const rightHoverZoneStyle = css({
left: `unset`,
top: 'unset',
bottom: `var(${HOVER_ZONE_BOTTOM})`
});
// gap between node boundary and drop indicator/drop zone
const GAP = 4;
const dropTargetLayoutHintStyle = css({
height: '100%',
position: 'absolute',
borderRight: `${"var(--ds-border-width, 1px)"} dashed ${"var(--ds-border-focused, #4688EC)"}`,
width: 0,
left: 0
});
const dropTargetLayoutHintLeftStyle = css({
left: 'unset',
right: 0
});
const defaultNodeDimension = {
width: '0',
height: '0',
top: 'unset',
bottom: 'unset'
};
const getWidthOffset = (node, width, position) => {
if (['mediaSingle', 'table', 'embedCard'].includes(node.type.name) ||
// block card (without datasource) is positioned left-aligned, hence share the same logic as align-start
node.type.name === 'blockCard' && !node.attrs.datasource) {
const isLeftPosition = position === 'left';
if (node.attrs.layout === 'align-start' || node.type.name === 'blockCard') {
return isLeftPosition ? `-0.5*(var(--ak-editor--line-length) - ${width})` : `0.5*(var(--ak-editor--line-length) - ${width})`;
} else if ((node === null || node === void 0 ? void 0 : node.attrs.layout) === 'align-end') {
return isLeftPosition ? `0.5*(var(--ak-editor--line-length) - ${width})` : `-0.5*(var(--ak-editor--line-length) - ${width})`;
}
}
if (node.type.name === 'bodiedExtension' || node.type.name === 'extension') {
return '-12px';
}
};
const TABLE_NUMBERED_COLUMN_WIDTH = 42;
export const InlineDropTarget = ({
api,
nextNode,
position,
anchorRectCache,
getPos
}) => {
const ref = useRef(null);
const [isDraggedOver, setIsDraggedOver] = useState(false);
const anchorName = useMemo(() => {
if (expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) {
var _getPos;
return nextNode ? (api === null || api === void 0 ? void 0 : api.core.actions.getAnchorIdForNode(nextNode, (_getPos = getPos()) !== null && _getPos !== void 0 ? _getPos : -1)) || '' : '';
}
return nextNode ? getNodeAnchor(nextNode) : '';
}, [api, getPos, nextNode]);
const [isActiveAnchor] = useActiveAnchorTracker(anchorName);
const isLeftPosition = position === 'left';
const nodeDimension = useMemo(() => {
if (!nextNode) {
return defaultNodeDimension;
}
const nextNodePos = getPos();
let innerContainerWidth = null;
let targetAnchorName = anchorName;
if (['blockCard', 'embedCard', 'extension'].includes(nextNode.type.name)) {
if (nextNode.attrs.layout === 'wide') {
innerContainerWidth = `max(var(--ak-editor--legacy-breakout-wide-layout-width), var(--ak-editor--line-length))`;
} else if (nextNode.attrs.layout === 'full-width') {
innerContainerWidth = `min(calc(100cqw - ${akEditorBreakoutPadding}px), 1800px)`;
}
if (nextNode.type.name === 'blockCard' && !nextNode.attrs.layout && nextNode.attrs.datasource) {
// block card with sourceNode and without layout has different width in full-width vs fixed-width editor
// Hence we need to set it based on editor mode
innerContainerWidth = 'var(--ak-editor-block-card-width)';
}
if (nextNode.type.name === 'embedCard' && ['center', 'align-start', 'align-end'].includes(nextNode.attrs.layout)) {
const percentageWidth = ((parseFloat(nextNode.attrs.width) || 100) / 100).toFixed(2);
innerContainerWidth = `calc(var(--ak-editor--line-length) * ${percentageWidth})`;
}
} else if (nextNode.type.name === 'table' && nextNode.firstChild) {
const tableWidthAnchor = expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) ? typeof nextNodePos === 'number' ? (api === null || api === void 0 ? void 0 : api.core.actions.getAnchorIdForNode(nextNode.firstChild, nextNodePos + 1)) || '' : '' : getNodeAnchor(nextNode.firstChild);
const isNumberColumnEnabled = Boolean(nextNode.attrs.isNumberColumnEnabled);
if (isAnchorSupported()) {
innerContainerWidth = isNumberColumnEnabled ? `calc(anchor-size(${tableWidthAnchor} width) + ${TABLE_NUMBERED_COLUMN_WIDTH}px)` : `anchor-size(${tableWidthAnchor} width)`;
} else {
var _anchorRectCache$getR;
innerContainerWidth = `${((anchorRectCache === null || anchorRectCache === void 0 ? void 0 : (_anchorRectCache$getR = anchorRectCache.getRect(tableWidthAnchor)) === null || _anchorRectCache$getR === void 0 ? void 0 : _anchorRectCache$getR.width) || 0) + TABLE_NUMBERED_COLUMN_WIDTH}px`;
}
if (nextNode.attrs.width) {
// when the table has horizontal scroll
innerContainerWidth = `min(${nextNode.attrs.width}px, ${innerContainerWidth})`;
}
} else if (nextNode.type.name === 'mediaSingle' && nextNode.firstChild) {
if (expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) {
var _nextNode$firstChild;
// check pos is a number
if (typeof nextNodePos === 'number' && ((_nextNode$firstChild = nextNode.firstChild) === null || _nextNode$firstChild === void 0 ? void 0 : _nextNode$firstChild.type.name) === 'media') {
targetAnchorName = (api === null || api === void 0 ? void 0 : api.core.actions.getAnchorIdForNode(nextNode.firstChild, nextNodePos + 1)) || '';
}
} else {
targetAnchorName = getNodeAnchor(nextNode.firstChild);
}
}
// Set the height target anchor name to the first or last column of the layout section so that it also works for stacked layout
let heightTargetAnchorName = targetAnchorName;
if (nextNode.type.name === 'layoutSection' && nextNode.firstChild && nextNode.lastChild) {
if (isLeftPosition) {
if (expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) {
if (typeof nextNodePos === 'number') {
heightTargetAnchorName = (api === null || api === void 0 ? void 0 : api.core.actions.getAnchorIdForNode(nextNode.firstChild, nextNodePos + 1)) || '';
} else {
heightTargetAnchorName = '';
}
} else {
heightTargetAnchorName = getNodeAnchor(nextNode.firstChild);
}
} else {
if (expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) {
if (typeof nextNodePos === 'number') {
const lastNodeStartPos = nextNode.content.size - nextNode.lastChild.nodeSize;
heightTargetAnchorName = (api === null || api === void 0 ? void 0 : api.core.actions.getAnchorIdForNode(nextNode.lastChild, lastNodeStartPos + 1)) || '';
} else {
heightTargetAnchorName = '';
}
} else {
heightTargetAnchorName = getNodeAnchor(nextNode.lastChild);
}
}
}
if (isAnchorSupported()) {
const width = innerContainerWidth || `anchor-size(${targetAnchorName} width)`;
const height = `anchor-size(${heightTargetAnchorName} height)`;
return {
width,
height,
top: 'anchor(top)',
bottom: 'anchor(bottom)',
widthOffset: getWidthOffset(nextNode, width, position)
};
}
if (anchorRectCache) {
const nodeRect = anchorRectCache.getRect(targetAnchorName);
const width = innerContainerWidth || `${(nodeRect === null || nodeRect === void 0 ? void 0 : nodeRect.width) || 0}px`;
const top = nodeRect !== null && nodeRect !== void 0 && nodeRect.top ? `${nodeRect === null || nodeRect === void 0 ? void 0 : nodeRect.top}px` : 'unset';
const bottom = `100% - ${(nodeRect === null || nodeRect === void 0 ? void 0 : nodeRect.bottom) || 0}px + ${GAP}px`;
let height = `${(nodeRect === null || nodeRect === void 0 ? void 0 : nodeRect.height) || 0}px`;
if (heightTargetAnchorName !== targetAnchorName) {
const nodeHeightRect = anchorRectCache.getRect(heightTargetAnchorName);
height = `${(nodeHeightRect === null || nodeHeightRect === void 0 ? void 0 : nodeHeightRect.height) || 0}px + ${GAP}px`;
}
return {
width,
height,
top,
bottom,
widthOffset: getWidthOffset(nextNode, width, position)
};
}
return defaultNodeDimension;
}, [nextNode, anchorName, anchorRectCache, getPos, api, isLeftPosition, position]);
const onDrop = useCallback(() => {
var _api$blockControls;
const {
activeNode
} = (api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.sharedState.currentState()) || {};
if (!activeNode) {
return;
}
const toPos = getPos();
let mappedTo;
if (activeNode && toPos !== undefined) {
var _api$core, _api$core2;
const {
pos: start
} = activeNode;
const moveToEnd = position === 'right';
api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
tr
}) => {
var _api$blockControls2, _api$blockControls2$c;
api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : (_api$blockControls2$c = _api$blockControls2.commands) === null || _api$blockControls2$c === void 0 ? void 0 : _api$blockControls2$c.moveToLayout(start, toPos, {
moveToEnd
})({
tr
});
const insertLayoutStep = getInsertLayoutStep(tr);
mappedTo = insertLayoutStep === null || insertLayoutStep === void 0 ? void 0 : insertLayoutStep.from;
return tr;
});
api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
tr
}) => {
if (mappedTo !== undefined) {
updateSelection(tr, mappedTo, moveToEnd);
}
return tr;
});
}
}, [api, getPos, position]);
const hoverZoneRectStyle = useMemo(() => {
const isLayoutNode = (nextNode === null || nextNode === void 0 ? void 0 : nextNode.type.name) === 'layoutSection';
const layoutAdjustment = isLayoutNode ? {
width: 11,
height: 4,
top: 6,
bottom: 2
} : undefined;
return {
[HOVER_ZONE_WIDTH]: nodeDimension.widthOffset ? `calc((100% - ${nodeDimension.width})/2 - ${GAP}px + ${nodeDimension.widthOffset} - ${(layoutAdjustment === null || layoutAdjustment === void 0 ? void 0 : layoutAdjustment.width) || 0}px)` : `calc((100% - ${nodeDimension.width})/2 - ${GAP}px - ${(layoutAdjustment === null || layoutAdjustment === void 0 ? void 0 : layoutAdjustment.width) || 0}px)`,
[HOVER_ZONE_HEIGHT]: `calc(${nodeDimension.height} + ${(layoutAdjustment === null || layoutAdjustment === void 0 ? void 0 : layoutAdjustment.height) || 0}px)`,
[HOVER_ZONE_TOP]: `calc(${nodeDimension.top} + ${(layoutAdjustment === null || layoutAdjustment === void 0 ? void 0 : layoutAdjustment.top) || 0}px)`,
[HOVER_ZONE_BOTTOM]: `calc(${nodeDimension.bottom} - ${(layoutAdjustment === null || layoutAdjustment === void 0 ? void 0 : layoutAdjustment.bottom) || 0}px)`,
[HOVER_ZONE_ANCHOR_NAME]: anchorName
};
}, [nextNode === null || nextNode === void 0 ? void 0 : nextNode.type.name, nodeDimension, anchorName]);
const dropIndicatorPos = useMemo(() => {
return isLeftPosition ? 'right' : 'left';
}, [isLeftPosition]);
useEffect(() => {
if (ref.current) {
return dropTargetForElements({
element: ref.current,
onDragEnter: () => {
setIsDraggedOver(true);
},
onDragLeave: () => {
setIsDraggedOver(false);
},
onDrop
});
}
}, [onDrop, setIsDraggedOver]);
return jsx("div", {
ref: ref,
"data-testid": `drop-target-hover-zone-${position}`,
css: [hoverZoneCommonStyle, isLeftPosition ? leftHoverZoneStyle : rightHoverZoneStyle]
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
,
style: hoverZoneRectStyle
}, isDraggedOver ? jsx(DropIndicator, {
edge: dropIndicatorPos
}) : isActiveAnchor && jsx("div", {
"data-testid": "block-ctrl-drop-hint",
css: [dropTargetLayoutHintStyle, isLeftPosition && dropTargetLayoutHintLeftStyle]
}));
};