UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

292 lines (290 loc) 13 kB
import _extends from "@babel/runtime/helpers/extends"; /** * @jsxRuntime classic * @jsx jsx */ import { Fragment, 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 { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { DropIndicator } from '@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box'; import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { layers } from '@atlaskit/theme/constants'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; 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 { shouldAllowInlineDropTarget } from '../pm-plugins/utils/inline-drop-target'; import { getNestedNodeLeftPaddingMargin } from './consts'; import { InlineDropTarget } from './inline-drop-target'; const DEFAULT_DROP_INDICATOR_WIDTH = 760; const EDITOR_BLOCK_CONTROLS_DROP_INDICATOR_WIDTH = '--editor-block-controls-drop-indicator-width'; const EDITOR_BLOCK_CONTROLS_DROP_TARGET_LEFT_MARGIN = '--editor-block-controls-drop-target-leftMargin'; const EDITOR_BLOCK_CONTROLS_DROP_TARGET_ZINDEX = '--editor-block-controls-drop-target-zindex'; export const EDITOR_BLOCK_CONTROLS_DROP_INDICATOR_OFFSET = '--editor-block-controls-drop-indicator-offset'; export const EDITOR_BLOCK_CONTROLS_DROP_INDICATOR_GAP = '--editor-block-controls-drop-indicator-gap'; const styleDropTarget = css({ marginLeft: `calc(-1 * var(${EDITOR_BLOCK_CONTROLS_DROP_TARGET_LEFT_MARGIN}, 0))`, paddingLeft: `var(${EDITOR_BLOCK_CONTROLS_DROP_TARGET_LEFT_MARGIN}, 0)`, position: 'absolute', left: '0', display: 'block', zIndex: `var(${EDITOR_BLOCK_CONTROLS_DROP_TARGET_ZINDEX}, 110)`, transform: `translateY(var(${EDITOR_BLOCK_CONTROLS_DROP_INDICATOR_OFFSET}, 0))` }); const styleDropIndicator = css({ height: '100%', margin: '0 auto', position: 'relative', width: `var(${EDITOR_BLOCK_CONTROLS_DROP_INDICATOR_WIDTH}, 100%)`, display: 'none' }); const styleDropIndicatorVisible = css({ display: 'block' }); const nestedDropIndicatorStyle = css({ position: 'relative' }); const dropZoneStyles = css({ margin: 0, position: 'absolute', width: '100%', zIndex: 110, minHeight: '4px' }); const nestedDropZoneStyle = css({ left: '4px', right: '4px', width: 'unset' }); const enableDropZone = ['paragraph', 'mediaSingle', 'heading', 'codeBlock', 'decisionList', 'bulletList', 'orderedList', 'taskList', 'extension', 'blockCard']; const enableDropZoneNext = ['paragraph', 'mediaSingle', 'heading', 'codeBlock', 'decisionList', 'bulletList', 'orderedList', 'taskList', 'extension', 'blockCard', 'syncBlock']; const getEnableDropZone = () => { if (editorExperiment('platform_synced_block_patch_6', true, { exposure: true })) { return enableDropZoneNext; } return enableDropZone; }; // This z index is used in container like layout const fullHeightStyleAdjustZIndexStyle = css({ zIndex: 0 }); const HoverZone = ({ onDragEnter, onDragLeave, onDrop, node, pos, parent, editorWidth, anchorRectCache, position, isNestedDropTarget, dropTargetStyle, api }) => { const ref = useRef(null); const isRemainingheight = dropTargetStyle === 'remainingHeight'; const anchorName = useMemo(() => { if (expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) { if (node && typeof pos === 'number') { const posOffset = position === 'upper' ? -node.nodeSize : 0; return (api === null || api === void 0 ? void 0 : api.core.actions.getAnchorIdForNode(node, pos + posOffset)) || ''; } return ''; } return node ? getNodeAnchor(node) : ''; }, [api, node, pos, position]); const [_isActive, setActiveAnchor] = useActiveAnchorTracker(anchorName); const isInsideBodiedSyncBlock = parent && parent.type.name === 'bodiedSyncBlock' && editorExperiment('platform_synced_block', true) && editorExperiment('platform_synced_block_patch_6', true, { exposure: true }); useEffect(() => { if (ref.current) { return dropTargetForElements({ element: ref.current, onDragEnter: () => { if ((!isNestedDropTarget || isInsideBodiedSyncBlock) && editorExperiment('advanced_layouts', true)) { setActiveAnchor(); } onDragEnter(); }, onDragLeave, onDrop }); } }, [isNestedDropTarget, isInsideBodiedSyncBlock, onDragEnter, onDragLeave, onDrop, setActiveAnchor]); const hoverZoneUpperStyle = useMemo(() => { const heightStyleOffset = `var(--editor-block-controls-drop-indicator-gap, 0)/2`; const transformOffset = `var(${EDITOR_BLOCK_CONTROLS_DROP_INDICATOR_OFFSET}, 0)`; const heightStyle = anchorName && getEnableDropZone().includes((node === null || node === void 0 ? void 0 : node.type.name) || '') ? isAnchorSupported() ? `calc(anchor-size(${anchorName} height)/2 + ${heightStyleOffset})` : `calc(${((anchorRectCache === null || anchorRectCache === void 0 ? void 0 : anchorRectCache.getHeight(anchorName)) || 0) / 2}px + ${heightStyleOffset})` : '4px'; const transform = position === 'upper' ? `translateY(calc(-100% + ${transformOffset}))` : `translateY(${transformOffset})`; return css({ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values height: heightStyle, // eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values transform: transform, // eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values maxWidth: `${editorWidth || 0}px` }); }, [anchorName, anchorRectCache, editorWidth, node === null || node === void 0 ? void 0 : node.type.name, position]); /** * 1. Above the last empty line * 2. Below the last element * * Both cases will take the remaining height of the the container */ const heightStyle = useMemo(() => { // only apply upper drop zone if (isRemainingheight && position === 'upper') { // previous node const anchorName = node ? expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) ? (api === null || api === void 0 ? void 0 : api.core.actions.getAnchorIdForNode(node, pos || -1)) || '' : getNodeAnchor(node) : ''; let top = 'unset'; if (anchorName) { const enabledDropZone = getEnableDropZone().includes((node === null || node === void 0 ? void 0 : node.type.name) || ''); if (isAnchorSupported()) { top = enabledDropZone ? `calc(anchor(${anchorName} 50%))` : `calc(anchor(${anchorName} bottom) - 4px)`; } else if (anchorRectCache) { const preNodeTopPos = anchorRectCache.getTop(anchorName) || 0; const prevNodeHeight = anchorRectCache.getHeight(anchorName) || 0; top = enabledDropZone ? `calc(${preNodeTopPos}px + ${prevNodeHeight / 2}px)` : `calc(${preNodeTopPos}px + ${prevNodeHeight}px - 4px)`; } else { // Should not happen return null; } } else { // first empty paragraph top = '4px'; } return css({ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values top: top, bottom: '4px', height: 'unset', zIndex: 10, transform: 'none' }); } return null; }, [anchorRectCache, api, isRemainingheight, node, pos, position]); const isFullHeightInLayout = isRemainingheight && (parent === null || parent === void 0 ? void 0 : parent.type.name) === 'layoutColumn'; return jsx("div", { ref: ref // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop , className: `drop-target-hover-zone-${position}`, "data-testid": `drop-target-zone-${position}` // eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage , css: [dropZoneStyles, isNestedDropTarget && nestedDropZoneStyle, // eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage hoverZoneUpperStyle, // eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage heightStyle, isFullHeightInLayout && fullHeightStyleAdjustZIndexStyle] }); }; export const DropTarget = props => { var _api$blockControls; const { api, getPos, prevNode, nextNode, parentNode, formatMessage, anchorRectCache, dropTargetStyle = 'default', isSameLayout } = props; const [isDraggedOver, setIsDraggedOver] = useState(false); const { lineLength } = useSharedPluginStateWithSelector(api, ['width'], states => { var _states$widthState; return { lineLength: ((_states$widthState = states.widthState) === null || _states$widthState === void 0 ? void 0 : _states$widthState.lineLength) || DEFAULT_DROP_INDICATOR_WIDTH }; }); const isNestedDropTarget = (parentNode === null || parentNode === void 0 ? void 0 : parentNode.type.name) !== 'doc'; const { activeNode } = (api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.sharedState.currentState()) || {}; const onDrop = () => { if (!activeNode) { return; } const pos = getPos(); if (activeNode && pos !== undefined) { var _api$core, _api$blockControls2, _api$blockControls2$c; const { pos: start } = activeNode; api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(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.moveNode(start, pos, undefined, formatMessage)); } }; const dynamicStyle = { width: isNestedDropTarget ? 'unset' : '100%', [EDITOR_BLOCK_CONTROLS_DROP_INDICATOR_WIDTH]: isNestedDropTarget ? '100%' : `${lineLength || DEFAULT_DROP_INDICATOR_WIDTH}px`, [EDITOR_BLOCK_CONTROLS_DROP_TARGET_LEFT_MARGIN]: isNestedDropTarget ? getNestedNodeLeftPaddingMargin(parentNode === null || parentNode === void 0 ? void 0 : parentNode.type.name) : '0', [EDITOR_BLOCK_CONTROLS_DROP_TARGET_ZINDEX]: layers.navigation() }; const isShowInlineDropTarget = shouldAllowInlineDropTarget(isNestedDropTarget, nextNode, isSameLayout, activeNode, parentNode); return jsx(Fragment, null, jsx(HoverZone // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , { onDragEnter: () => setIsDraggedOver(true) // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , onDragLeave: () => setIsDraggedOver(false), onDrop: onDrop, node: prevNode, pos: getPos(), editorWidth: lineLength, anchorRectCache: anchorRectCache, position: "upper", isNestedDropTarget: isNestedDropTarget, dropTargetStyle: dropTargetStyle, api: api }), jsx("div", { // eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage css: [styleDropTarget, isNestedDropTarget && nestedDropIndicatorStyle] // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop , style: dynamicStyle, "data-testid": "block-ctrl-drop-target" }, jsx("div", { css: [styleDropIndicator, isDraggedOver && styleDropIndicatorVisible], "data-testid": "block-ctrl-drop-indicator" }, jsx(DropIndicator, { edge: "bottom" }))), dropTargetStyle !== 'remainingHeight' && jsx(HoverZone // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , { onDragEnter: () => setIsDraggedOver(true) // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , onDragLeave: () => setIsDraggedOver(false), onDrop: onDrop, node: nextNode, pos: getPos(), parent: parentNode, editorWidth: lineLength, anchorRectCache: anchorRectCache, position: "lower", isNestedDropTarget: isNestedDropTarget, api: api }), isShowInlineDropTarget && jsx(Fragment, null, jsx(InlineDropTarget // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading , _extends({}, props, { position: "left" })), jsx(InlineDropTarget // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading , _extends({}, props, { position: "right" })))); };