@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
138 lines (134 loc) • 5.84 kB
JavaScript
import { shouldBeSticky } from '../../pm-plugins/utils/drag-handle-positions';
import { dragHandleGap, topPositionAdjustment } from '../consts';
export const getAbsoluteSurfacePlacement = ({
offsetParentRect,
offsetParentScrollLeft,
offsetParentScrollTop,
placement
}) => {
var _offsetParentRect$lef, _offsetParentRect$top;
return {
...placement,
left: placement.left - ((_offsetParentRect$lef = offsetParentRect === null || offsetParentRect === void 0 ? void 0 : offsetParentRect.left) !== null && _offsetParentRect$lef !== void 0 ? _offsetParentRect$lef : 0) + offsetParentScrollLeft,
top: placement.top - ((_offsetParentRect$top = offsetParentRect === null || offsetParentRect === void 0 ? void 0 : offsetParentRect.top) !== null && _offsetParentRect$top !== void 0 ? _offsetParentRect$top : 0) + offsetParentScrollTop
};
};
/**
* Positions the surface beside the legacy drag-handle slot, on either the left or right edge
* of the target node. Node-type-specific gap/top-adjustment maths (layouts, tables, media, cards,
* extensions, etc.) live in one place so left/right edge cases can't drift out of sync.
*
* The left side grows away from the node without knowing its own width, so it needs
* `translateX(-100%)` to anchor its right edge at `nodeRect.left` and extend backward. The right
* side grows in the normal box-flow direction (away from the node, into the margin), so the gap
* can be folded straight into `left` with no transform needed.
*/
export const getSurfacePlacement = ({
layout,
nodeRect,
nodeType,
nodeTypeWithLevel,
parentNodeType,
side
}) => {
if (nodeType === 'layoutColumn') {
// nodeRect.left + width/2 === nodeRect.right - width/2 — side-independent.
return {
left: nodeRect.left + nodeRect.width / 2,
top: nodeRect.top,
transform: 'translate(-50%, -50%)'
};
}
const gap = dragHandleGap(nodeType, parentNodeType);
const top = nodeRect.top + topPositionAdjustment(nodeTypeWithLevel, layout);
const height = shouldBeSticky(nodeType) ? nodeRect.height : undefined;
return side === 'right' ? {
left: nodeRect.right + gap,
top,
transform: '',
height
} : {
left: nodeRect.left,
top,
transform: `translateX(calc(-100% - ${gap}px))`,
height
};
};
/**
* The insets to apply to a surface wrapper, plus whether that wrapper spans its target node.
*
* Two kinds of placement produce this. A measured one resolves to pixel numbers from a
* `getBoundingClientRect` snapshot, so it is only as fresh as the last render and needs observers
* to keep it honest. An anchored one resolves to `anchor()` functions that the browser re-evaluates
* on every layout change, so it stays attached to its node with nothing re-rendering.
*
* `isSticky` is carried alongside the style rather than read back out of it because the two variants
* express the same stretch differently — a measured `height` versus an anchored `bottom`.
*/
/**
* Adapts a measured placement to the shape the surface components consume. Sticky-eligible node
* types are the ones `getSurfacePlacement` gave a `height` to.
*/
export const toMeasuredSurfaceWrapperPlacement = placement => ({
isSticky: placement.height !== undefined,
style: {
height: placement.height,
left: placement.left,
top: placement.top,
transform: placement.transform
}
});
/**
* Where a surface goes when its anchor stops resolving, as the fallback argument to the block-axis
* `anchor()` calls below.
*
* An anchor is invalid once its element stops generating a box e.g. (`display: none`) / removed node
* Currently, anchor fallback is not unstable with some of the position-visibility options, so here
* we manually set a fallback of -9999 so that it will render offscreen out of the viewport
* without causing extra overflows
*/
const INVALID_ANCHOR_OFFSCREEN_PX = 9999;
const INVALID_ANCHOR_FALLBACK_TOP = `-${INVALID_ANCHOR_OFFSCREEN_PX}px`;
const INVALID_ANCHOR_FALLBACK_BOTTOM = `${INVALID_ANCHOR_OFFSCREEN_PX}px`;
export const getAnchoredSurfacePlacement = ({
anchorName,
layout,
nodeType,
nodeTypeWithLevel,
parentNodeType,
side
}) => {
if (nodeType === 'layoutColumn') {
// The column's horizontal midpoint, matching the measured `nodeRect.left + width / 2` and the
// legacy drag handle's own layout-column formula. Side-independent.
return {
isSticky: false,
style: {
left: `calc((anchor(${anchorName} left) + anchor(${anchorName} right)) / 2)`,
positionAnchor: anchorName,
top: `anchor(${anchorName} top, ${INVALID_ANCHOR_FALLBACK_TOP})`,
transform: 'translate(-50%, -50%)'
}
};
}
const gap = dragHandleGap(nodeType, parentNodeType);
const isSticky = shouldBeSticky(nodeType);
return {
isSticky,
style: {
// A sticky wrapper is stretched with `bottom` instead of a measured `height`. Pairing it
// with the adjusted `top` keeps the box ending exactly where the node does, however far
// the top adjustment pushed the start down and however much the node later grows.
...(isSticky ? {
bottom: `anchor(${anchorName} bottom, ${INVALID_ANCHOR_FALLBACK_BOTTOM})`
} : {}),
left: side === 'right' ? `calc(anchor(${anchorName} right) + ${gap}px)` : `anchor(${anchorName} left)`,
positionAnchor: anchorName,
top: `calc(anchor(${anchorName} top, ${INVALID_ANCHOR_FALLBACK_TOP}) + ${topPositionAdjustment(nodeTypeWithLevel, layout)}px)`,
// The left side grows away from the node without knowing its own width, so it pulls itself
// back past its own box and the gap. The right side grows into the margin, so the gap is
// already folded into `left` above.
transform: side === 'right' ? '' : `translateX(calc(-100% - ${gap}px))`
}
};
};