@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
246 lines (244 loc) • 10.8 kB
JavaScript
/**
* @jsxRuntime classic
* @jsx jsx
*/
import { useCallback, useEffect, useLayoutEffect, 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 { layoutBreakpointWidth } 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';
import { getAnchorAttrName } from './utils/dom-attr-name';
// 8px gap + 16px on left and right
const DROP_TARGET_LAYOUT_DROP_ZONE_WIDTH = 40;
const dropTargetLayoutStyle = css({
height: '100%',
width: `${DROP_TARGET_LAYOUT_DROP_ZONE_WIDTH}px`,
transform: 'translateX(-50%)',
zIndex: 120,
position: 'relative',
display: 'flex',
justifyContent: 'center'
});
const dropTargetLayoutHintStyle = css({
height: '100%',
position: 'relative',
borderRight: `${"var(--ds-border-width, 1px)"} dashed ${"var(--ds-border-focused, #4688EC)"}`,
width: 0
});
export const DropTargetLayout = props => {
var _ref$current, _ref$current$parentEl, _ref$current$parentEl2, _api$blockControls;
const {
api,
getPos,
parent,
anchorRectCache
} = props;
const ref = useRef(null);
const [isDraggedOver, setIsDraggedOver] = useState(false);
const anchorName = getNodeAnchor(parent);
const nextNodeAnchorName = (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : (_ref$current$parentEl = _ref$current.parentElement) === null || _ref$current$parentEl === void 0 ? void 0 : (_ref$current$parentEl2 = _ref$current$parentEl.nextElementSibling) === null || _ref$current$parentEl2 === void 0 ? void 0 : _ref$current$parentEl2.getAttribute(getAnchorAttrName());
let height = '100%';
if (nextNodeAnchorName) {
if (isAnchorSupported()) {
height = `anchor-size(${nextNodeAnchorName} height)`;
} else if (anchorRectCache) {
const layoutColumnRect = anchorRectCache.getRect(nextNodeAnchorName);
height = `${(layoutColumnRect === null || layoutColumnRect === void 0 ? void 0 : layoutColumnRect.height) || 0}px`;
}
}
const dropTargetStackLayoutHintStyle = css({
// jest warning: JSDOM version (22) doesn't support the new @container CSS rule
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-container-queries, @atlaskit/ui-styling-standard/no-unsafe-values, @atlaskit/ui-styling-standard/no-imported-style-values
[`@container layout-area (max-width:${layoutBreakpointWidth.MEDIUM - 1}px)`]: {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values
height,
marginTop: `${"var(--ds-space-050, 4px)"}`
}
});
const [isActiveAnchor] = useActiveAnchorTracker(anchorName);
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 = useCallback(() => {
if (!activeNode) {
return;
}
const to = getPos();
let mappedTo;
if (to !== undefined) {
var _api$core, _api$core2;
const {
pos: from
} = activeNode;
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(from, to)({
tr
});
const insertColumnStep = getInsertLayoutStep(tr);
mappedTo = insertColumnStep === null || insertColumnStep === void 0 ? void 0 : insertColumnStep.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);
}
return tr;
});
}
}, [api, getPos, activeNode]);
useEffect(() => {
if (ref.current) {
return dropTargetForElements({
element: ref.current,
onDragEnter: () => {
setIsDraggedOver(true);
},
onDragLeave: () => {
setIsDraggedOver(false);
},
onDrop
});
}
}, [onDrop]);
if ((activeNode === null || activeNode === void 0 ? void 0 : activeNode.nodeType) === 'layoutSection') {
return null;
}
return jsx("div", {
ref: ref
// eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage
,
css: [dropTargetLayoutStyle, dropTargetStackLayoutHintStyle],
"data-testid": "block-ctrl-drop-indicator"
}, isDraggedOver ? jsx(DropIndicator, {
edge: "right",
gap: `-${DROP_TARGET_LAYOUT_DROP_ZONE_WIDTH}px`
}) : (isActiveAnchor || expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) && jsx("div", {
"data-testid": "block-ctrl-drop-hint",
css: dropTargetLayoutHintStyle
}));
};
export const DropTargetLayoutNativeAnchorSupport = props => {
var _api$blockControls3;
const {
api,
getPos,
parent,
anchorRectCache
} = props;
const ref = useRef(null);
const [isDraggedOver, setIsDraggedOver] = useState(false);
const anchorName = getNodeAnchor(parent);
const [nextNodeAnchorName, setNextNodeAnchorName] = useState(null);
const readNextNodeAnchor = useCallback(() => {
var _ref$current2, _ref$current2$parentE, _nextElementSibling$g;
const nextElementSibling = (_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : (_ref$current2$parentE = _ref$current2.parentElement) === null || _ref$current2$parentE === void 0 ? void 0 : _ref$current2$parentE.nextElementSibling;
const attrName = getAnchorAttrName();
const nextAnchorName = (_nextElementSibling$g = nextElementSibling === null || nextElementSibling === void 0 ? void 0 : nextElementSibling.getAttribute(attrName)) !== null && _nextElementSibling$g !== void 0 ? _nextElementSibling$g : null;
setNextNodeAnchorName(prev => prev === nextAnchorName ? prev : nextAnchorName);
}, []);
const height = useMemo(() => {
if (nextNodeAnchorName) {
if (isAnchorSupported()) {
return `anchor-size(${nextNodeAnchorName} height)`;
} else if (anchorRectCache) {
const layoutColumnRect = anchorRectCache.getRect(nextNodeAnchorName);
return `${(layoutColumnRect === null || layoutColumnRect === void 0 ? void 0 : layoutColumnRect.height) || 0}px`;
}
}
// Stacked mode fallback: minimal height to avoid oversized hint on first render
return '0px';
}, [nextNodeAnchorName, anchorRectCache]);
useLayoutEffect(() => {
const raf = requestAnimationFrame(() => {
readNextNodeAnchor();
});
return () => cancelAnimationFrame(raf);
}, [readNextNodeAnchor]);
const dropTargetStackLayoutHintStyle = css({
// jest warning: JSDOM version (22) doesn't support the new @container CSS rule
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-container-queries, @atlaskit/ui-styling-standard/no-unsafe-values, @atlaskit/ui-styling-standard/no-imported-style-values
[`@container layout-area (max-width:${layoutBreakpointWidth.MEDIUM - 1}px)`]: {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values
height,
marginTop: `${"var(--ds-space-050, 4px)"}`
}
});
const [isActiveAnchor] = useActiveAnchorTracker(anchorName);
const {
activeNode
} = (api === null || api === void 0 ? void 0 : (_api$blockControls3 = api.blockControls) === null || _api$blockControls3 === void 0 ? void 0 : _api$blockControls3.sharedState.currentState()) || {};
const onDrop = useCallback(() => {
if (!activeNode) {
return;
}
const to = getPos();
let mappedTo;
if (to !== undefined) {
var _api$core3, _api$core4;
const {
pos: from
} = activeNode;
api === null || api === void 0 ? void 0 : (_api$core3 = api.core) === null || _api$core3 === void 0 ? void 0 : _api$core3.actions.execute(({
tr
}) => {
var _api$blockControls4, _api$blockControls4$c;
api === null || api === void 0 ? void 0 : (_api$blockControls4 = api.blockControls) === null || _api$blockControls4 === void 0 ? void 0 : (_api$blockControls4$c = _api$blockControls4.commands) === null || _api$blockControls4$c === void 0 ? void 0 : _api$blockControls4$c.moveToLayout(from, to)({
tr
});
const insertColumnStep = getInsertLayoutStep(tr);
mappedTo = insertColumnStep === null || insertColumnStep === void 0 ? void 0 : insertColumnStep.from;
return tr;
});
api === null || api === void 0 ? void 0 : (_api$core4 = api.core) === null || _api$core4 === void 0 ? void 0 : _api$core4.actions.execute(({
tr
}) => {
if (mappedTo !== undefined) {
updateSelection(tr, mappedTo);
}
return tr;
});
}
}, [api, getPos, activeNode]);
useEffect(() => {
if (ref.current) {
return dropTargetForElements({
element: ref.current,
onDragEnter: () => {
setIsDraggedOver(true);
readNextNodeAnchor();
},
onDragLeave: () => {
setIsDraggedOver(false);
},
onDrop
});
}
}, [onDrop, readNextNodeAnchor]);
if ((activeNode === null || activeNode === void 0 ? void 0 : activeNode.nodeType) === 'layoutSection') {
return null;
}
return jsx("div", {
ref: ref
// eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage
,
css: [dropTargetLayoutStyle, dropTargetStackLayoutHintStyle],
"data-testid": "block-ctrl-drop-indicator"
}, isDraggedOver ? jsx(DropIndicator, {
edge: "right",
gap: `-${DROP_TARGET_LAYOUT_DROP_ZONE_WIDTH}px`
}) : (isActiveAnchor || expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) && jsx("div", {
"data-testid": "block-ctrl-drop-hint",
css: dropTargetLayoutHintStyle
}));
};