@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
190 lines (186 loc) • 7.3 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;
if (side === 'right') {
return {
left: nodeRect.right + gap,
top,
transform: '',
height
};
}
return {
left: nodeRect.left,
top,
transform: `translateX(calc(-100% - ${gap}px))`,
height
};
};
/**
* Expands a measured left surface from the available gutter boundary to the target node.
*/
export const getLeftGutterSurfacePlacement = ({
leftGutterBoundary,
...options
}) => {
const placement = getSurfacePlacement({
...options,
side: 'left'
});
if (options.nodeType === 'layoutColumn') {
return placement;
}
const gap = dragHandleGap(options.nodeType, options.parentNodeType);
return {
...placement,
left: leftGutterBoundary,
transform: '',
width: Math.max(0, options.nodeRect.left - leftGutterBoundary - gap)
};
};
/**
* 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,
...(placement.width !== undefined ? {
width: placement.width
} : {})
}
});
/**
* 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))`
}
};
};
/**
* Expands an anchored left surface across the available gutter without runtime measurement.
*/
export const getAnchoredLeftGutterSurfacePlacement = options => {
const placement = getAnchoredSurfacePlacement({
...options,
side: 'left'
});
if (options.nodeType === 'layoutColumn') {
return placement;
}
const gap = dragHandleGap(options.nodeType, options.parentNodeType);
return {
...placement,
style: {
...placement.style,
left: 0,
// In a `right` inset, `anchor(... left)` resolves from the containing block's right edge
// to the anchor's left edge. Adding the gap ends this wrapper just before the node.
right: `calc(anchor(${options.anchorName} left) + ${gap}px)`,
transform: ''
}
};
};