UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

243 lines 13.1 kB
/* block-controls-right-surfaces.tsx generated by @compiled/babel-plugin v3.0.2 */ import { ax, ix } from "@compiled/react/runtime"; import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { bind } from 'bind-event-listener'; import { getDocument } from '@atlaskit/browser-apis'; import { BLOCK_CONTROL_UI_CONTEXT } from '@atlaskit/editor-common/block-controls/block-control-ui-context'; import { BLOCK_CONTROLS_RIGHT_SURFACE } from '@atlaskit/editor-common/block-controls/surface-keys'; import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { isCSSAnchorSupported } from '@atlaskit/editor-common/styles'; import { createSurfaceContext } from '@atlaskit/editor-ui-control-model/create-surface-context'; import { willSurfaceRender } from '@atlaskit/editor-ui-control-model/surface-renderer'; import { getNodeTypeWithLevel } from '../pm-plugins/decorations-common'; import { BlockControlsRightSurface } from './block-controls-right-surface'; import { createBlockControlsSurfaceContextForPosition } from './block-controls-surface-context'; import { getBlockControlsSurfaceTargets } from './block-controls-surface-targets'; import { getNodeContentElement } from './utils/get-node-content-element'; import { getAbsoluteSurfacePlacement, getAnchoredSurfacePlacement, getSurfacePlacement, toMeasuredSurfaceWrapperPlacement } from './utils/get-surface-placement'; import { hasSurfaceControls } from './utils/has-surface-controls'; const EMPTY_SURFACE_POSITIONS = []; const EMPTY_SURFACE_ANCHORS = new Map(); const EMPTY_SURFACE_COMPONENTS = []; const EMPTY_MEASURED_SURFACES = []; /** * Registry-backed right surface tree. Mirrors the left surface's single-tree approach, but keeps its * own components lookup, targets, placements and measurement lifecycle so the right surface never * depends on the left surface's render pass. * * Targets are placed one of two ways. Where the node's anchor name can be relied on, the surface is * positioned with `anchor()` and the browser keeps it attached with nothing re-rendering. Everything * else is measured and kept honest by the observers below. See `hasInnerContentContainer`. */ export const BlockControlsRightSurfaces = ({ api, editorView }) => { var _api$uiControlRegistr, _api$uiControlRegistr2; const { activeNode, surfaceAnchors, surfaceActiveNodes, surfaceNodePositions } = useSharedPluginStateWithSelector(api, ['blockControls'], states => { var _states$blockControls, _states$blockControls2, _states$blockControls3, _states$blockControls4, _states$blockControls5, _states$blockControls6; return { activeNode: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.activeNode, surfaceActiveNodes: (_states$blockControls2 = states.blockControlsState) === null || _states$blockControls2 === void 0 ? void 0 : _states$blockControls2.surfaceActiveNodes, surfaceAnchors: (_states$blockControls3 = (_states$blockControls4 = states.blockControlsState) === null || _states$blockControls4 === void 0 ? void 0 : _states$blockControls4.surfaceAnchors) !== null && _states$blockControls3 !== void 0 ? _states$blockControls3 : EMPTY_SURFACE_ANCHORS, surfaceNodePositions: (_states$blockControls5 = (_states$blockControls6 = states.blockControlsState) === null || _states$blockControls6 === void 0 ? void 0 : _states$blockControls6.surfaceNodePositions) !== null && _states$blockControls5 !== void 0 ? _states$blockControls5 : EMPTY_SURFACE_POSITIONS }; }); const components = (_api$uiControlRegistr = (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(BLOCK_CONTROLS_RIGHT_SURFACE)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : EMPTY_SURFACE_COMPONENTS; const targets = useMemo(() => { const candidates = new Set(surfaceNodePositions); return getBlockControlsSurfaceTargets(activeNode, surfaceNodePositions).filter(({ position }) => candidates.has(position)); }, [activeNode, surfaceNodePositions]); const surfaces = useMemo(() => targets.flatMap(target => { var _surfaceActiveNodes$g; const context = createBlockControlsSurfaceContextForPosition(editorView.state, target.position, (_surfaceActiveNodes$g = surfaceActiveNodes === null || surfaceActiveNodes === void 0 ? void 0 : surfaceActiveNodes.get(target.position)) !== null && _surfaceActiveNodes$g !== void 0 ? _surfaceActiveNodes$g : activeNode); if (!context) { return []; } const surfaceContext = createSurfaceContext(BLOCK_CONTROL_UI_CONTEXT, context); if (!hasSurfaceControls(components) || !willSurfaceRender(components, BLOCK_CONTROLS_RIGHT_SURFACE, surfaceContext)) { return []; } // The cache supplies the same anchor names used by ProseMirror decorations. const anchorName = isCSSAnchorSupported() ? surfaceAnchors.get(target.position) : undefined; return [{ ...target, anchorName, blockControlsContext: context, surfaceContext }]; }), [activeNode, components, editorView, targets, surfaceAnchors, surfaceActiveNodes]); /** * Placements that need no measuring, so they are derived rather than stored: every inset is an * `anchor()` the browser re-resolves on every layout change. */ const anchoredPlacements = useMemo(() => new Map(surfaces.flatMap(({ anchorName, blockControlsContext, position }) => anchorName ? [[position, getAnchoredSurfacePlacement({ anchorName, nodeType: blockControlsContext.targetNode.type.name, nodeTypeWithLevel: getNodeTypeWithLevel(blockControlsContext.targetNode.node), layout: blockControlsContext.targetNode.node.attrs.layout, parentNodeType: blockControlsContext.targetNode.parentType === 'doc' ? undefined : blockControlsContext.targetNode.parentType, side: 'right' })]] : [])), [surfaces]); // Only the surfaces that still need measuring drive the effect below, so a document of anchored // blocks binds no scroll or resize listeners at all. // // The shared empty array matters: `surfaces` gets a new identity on every hover, since // `activeNode` is one of its dependencies. Returning a fresh `[]` would give the effect a // changed dependency each time and re-run it for nothing, which is the cost the anchored path // exists to avoid. const measuredSurfaces = useMemo(() => { const measured = surfaces.filter(({ anchorName }) => !anchorName); return measured.length === 0 ? EMPTY_MEASURED_SURFACES : measured; }, [surfaces]); const [placements, setPlacements] = useState(new Map()); const animationFrameRef = useRef(undefined); useLayoutEffect(() => { var _getDocument; if (measuredSurfaces.length === 0) { // Clear only when there is something to clear. Handing React a fresh Map every time would // fail its identity check and commit a render to replace one empty map with another. setPlacements(previous => previous.size > 0 ? new Map() : previous); return; } const measurementTargets = measuredSurfaces.flatMap(({ blockControlsContext, position }) => { 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 [{ blockControlsContext, position, 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 { blockControlsContext, position, targetElement, targetElementRoot } of measurementTargets) { var _targetElementRoot$ge; if (!targetElementRoot.isConnected || !targetElement.isConnected) { continue; } const placement = getSurfacePlacement({ 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, side: 'right' }); nextPlacements.set(position, 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, measuredSurfaces]); return /*#__PURE__*/React.createElement(React.Fragment, null, surfaces.map(({ position, source, surfaceContext }) => { var _activeNode$handleOpt, _anchoredPlacements$g; return /*#__PURE__*/React.createElement(BlockControlsRightSurface, { api: api, components: components, forceVisibleOnMouseOut: Boolean(activeNode === null || activeNode === void 0 ? void 0 : (_activeNode$handleOpt = activeNode.handleOptions) === null || _activeNode$handleOpt === void 0 ? void 0 : _activeNode$handleOpt.isFocused), key: position, placement: (_anchoredPlacements$g = anchoredPlacements.get(position)) !== null && _anchoredPlacements$g !== void 0 ? _anchoredPlacements$g : placements.get(position), source: source, surfaceContext: surfaceContext }); })); };