@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
243 lines • 13.1 kB
JavaScript
/* 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
});
}));
};