UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

287 lines (285 loc) 14.2 kB
/* 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] })); };