UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

163 lines (162 loc) • 7.56 kB
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; };