@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
163 lines (162 loc) • 7.56 kB
JavaScript
import { useLayoutEffect, useRef, useState } from 'react';
import { bind } from 'bind-event-listener';
import { getDocument } from '@atlaskit/browser-apis';
import { isCSSAnchorSupported } from '@atlaskit/editor-common/styles/native-anchor';
import { getNodeTypeWithLevel } from '../pm-plugins/decorations-common';
import { getNodeContentElement } from '../ui/utils/get-node-content-element';
import { getAbsoluteSurfacePlacement, getLeftGutterSurfacePlacement, getSurfacePlacement, toMeasuredSurfaceWrapperPlacement } from '../ui/utils/get-surface-placement';
const EMPTY_PLACEMENTS = new Map();
export const getSurfaceKey = (side, descriptor) => `${side}:${descriptor.anchorName}`;
/**
* Measures surface placements from DOM geometry when the browser cannot position surfaces with CSS
* anchors. Returns an empty map and binds nothing when anchors are supported. Both sides share one
* set of listeners, and measuring reruns only when a side's descriptor list changes, which the
* descriptor resolver keeps stable across unrelated updates.
*/
export const useMeasuredSurfacePlacements = (editorView, left, right) => {
const [placements, setPlacements] = useState(EMPTY_PLACEMENTS);
const animationFrameRef = useRef(undefined);
useLayoutEffect(() => {
var _getDocument;
// Anchor support is fixed for the session, so anchored surfaces never set measured state.
if (isCSSAnchorSupported()) {
return;
}
const descriptors = [...(left !== null && left !== void 0 ? left : []).map(descriptor => ({
descriptor,
side: 'left'
})), ...(right !== null && right !== void 0 ? right : []).map(descriptor => ({
descriptor,
side: 'right'
}))];
if (descriptors.length === 0) {
// Clear only when there is something to clear, so an empty map does not commit a render.
setPlacements(previous => previous.size > 0 ? EMPTY_PLACEMENTS : previous);
return;
}
const measurementTargets = descriptors.flatMap(({
descriptor,
side
}) => {
const {
blockControlsContext
} = descriptor;
const targetNodeElement = editorView.nodeDOM(blockControlsContext.targetNode.pos);
const targetElementRoot = targetNodeElement instanceof HTMLElement ? targetNodeElement : targetNodeElement === null || targetNodeElement === void 0 ? void 0 : targetNodeElement.parentElement;
if (!targetElementRoot) {
return [];
}
return [{
descriptor,
side,
targetElement: getNodeContentElement(targetElementRoot, blockControlsContext.targetNode.type.name),
targetElementRoot
}];
});
const offsetParent = editorView.dom.offsetParent;
const isDocumentOffsetParent = !offsetParent || offsetParent === ((_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.body);
const scrollingOffsetParent = offsetParent instanceof HTMLElement && !isDocumentOffsetParent ? offsetParent : undefined;
const updatePositions = () => {
var _scrollingOffsetParen, _scrollingOffsetParen2;
animationFrameRef.current = undefined;
const offsetParentRect = offsetParent === null || offsetParent === void 0 ? void 0 : offsetParent.getBoundingClientRect();
const offsetParentScrollTop = (_scrollingOffsetParen = scrollingOffsetParent === null || scrollingOffsetParent === void 0 ? void 0 : scrollingOffsetParent.scrollTop) !== null && _scrollingOffsetParen !== void 0 ? _scrollingOffsetParen : window.scrollY;
const offsetParentScrollLeft = (_scrollingOffsetParen2 = scrollingOffsetParent === null || scrollingOffsetParent === void 0 ? void 0 : scrollingOffsetParent.scrollLeft) !== null && _scrollingOffsetParen2 !== void 0 ? _scrollingOffsetParen2 : window.scrollX;
const nextPlacements = new Map();
for (const {
descriptor,
side,
targetElement,
targetElementRoot
} of measurementTargets) {
var _targetElementRoot$ge, _offsetParentRect$lef;
if (!targetElementRoot.isConnected || !targetElement.isConnected) {
continue;
}
const {
blockControlsContext,
isTopLevel
} = descriptor;
const options = {
layout: (_targetElementRoot$ge = targetElementRoot.getAttribute('layout')) !== null && _targetElementRoot$ge !== void 0 ? _targetElementRoot$ge : '',
nodeRect: targetElement.getBoundingClientRect(),
nodeType: blockControlsContext.targetNode.type.name,
nodeTypeWithLevel: getNodeTypeWithLevel(blockControlsContext.targetNode.node),
parentNodeType: blockControlsContext.targetNode.parentType === 'doc' ? undefined : blockControlsContext.targetNode.parentType
};
const placement = side === 'left' && isTopLevel ? getLeftGutterSurfacePlacement({
...options,
leftGutterBoundary: (_offsetParentRect$lef = offsetParentRect === null || offsetParentRect === void 0 ? void 0 : offsetParentRect.left) !== null && _offsetParentRect$lef !== void 0 ? _offsetParentRect$lef : 0
}) : getSurfacePlacement({
...options,
side
});
nextPlacements.set(getSurfaceKey(side, descriptor), toMeasuredSurfaceWrapperPlacement(getAbsoluteSurfacePlacement({
offsetParentRect,
offsetParentScrollLeft,
offsetParentScrollTop,
placement
})));
}
setPlacements(nextPlacements);
};
const schedulePositionUpdate = () => {
if (animationFrameRef.current === undefined) {
animationFrameRef.current = requestAnimationFrame(updatePositions);
}
};
updatePositions();
const resizeObserver = new ResizeObserver(schedulePositionUpdate);
const observedElements = new Set([editorView.dom]);
for (const {
targetElement
} of measurementTargets) {
observedElements.add(targetElement);
}
if (offsetParent instanceof HTMLElement) {
observedElements.add(offsetParent);
}
observedElements.forEach(element => resizeObserver.observe(element));
const unbindWindowResize = bind(window, {
type: 'resize',
listener: schedulePositionUpdate
});
const unbindWindowScroll = bind(window, {
type: 'scroll',
listener: schedulePositionUpdate,
options: {
capture: true,
passive: true
}
});
const visualViewport = window.visualViewport;
const unbindVisualViewportResize = visualViewport ? bind(visualViewport, {
type: 'resize',
listener: schedulePositionUpdate
}) : undefined;
const unbindVisualViewportScroll = visualViewport ? bind(visualViewport, {
type: 'scroll',
listener: schedulePositionUpdate
}) : undefined;
const unbindTransitionEnd = bind(editorView.dom, {
type: 'transitionend',
listener: schedulePositionUpdate,
options: {
capture: true
}
});
return () => {
if (animationFrameRef.current !== undefined) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = undefined;
}
resizeObserver.disconnect();
unbindWindowResize();
unbindWindowScroll();
unbindVisualViewportResize === null || unbindVisualViewportResize === void 0 ? void 0 : unbindVisualViewportResize();
unbindVisualViewportScroll === null || unbindVisualViewportScroll === void 0 ? void 0 : unbindVisualViewportScroll();
unbindTransitionEnd();
};
}, [editorView, left, right]);
return placements;
};