UNPKG

@atlaskit/editor-plugin-layout

Version:

Layout plugin for @atlaskit/editor-core

183 lines (181 loc) 9.05 kB
import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { bind } from 'bind-event-listener'; import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { DRAG_HANDLE_SELECTOR } from '@atlaskit/editor-common/styles'; import { Popup } from '@atlaskit/editor-common/ui'; import { ArrowKeyNavigationProvider, ArrowKeyNavigationType } from '@atlaskit/editor-common/ui-menu'; import { withReactEditorViewOuterListeners } from '@atlaskit/editor-common/ui-react'; import { UserIntentPopupWrapper } from '@atlaskit/editor-common/user-intent'; import { akEditorFloatingOverlapPanelZIndex } from '@atlaskit/editor-shared-styles'; import { ToolbarDropdownMenuProvider } from '@atlaskit/editor-toolbar'; import { SurfaceRenderer } from '@atlaskit/editor-ui-control-model'; import { getLayoutColumnMenuAnchorPos } from '../../pm-plugins/utils/layout-column-selection'; import { LAYOUT_COLUMN_MENU_FALLBACKS } from './components'; import { LAYOUT_COLUMN_MENU } from './keys'; import { calculateFallbackBottomPosition, getLayoutColumnMenuPositioningProps, shouldOpenLayoutColumnMenuBelow } from './utils'; const PopupWithListeners = withReactEditorViewOuterListeners(Popup); const TOOLBAR_MENU_SELECTOR = '[data-toolbar-component="menu"]'; const NESTED_DROPDOWN_MENU_SELECTOR = '[data-toolbar-nested-dropdown-menu]'; /** * Returns the drag handle button for the selected layout column. */ const getLayoutColumnMenuTarget = (editorView, selection, anchorPosFromHandle) => { var _columnDomRef$parentE; const anchorPos = selection && getLayoutColumnMenuAnchorPos(selection, anchorPosFromHandle); if (anchorPos === undefined) { return null; } const columnDomRef = editorView.nodeDOM(anchorPos); if (!(columnDomRef instanceof HTMLElement)) { return null; } const dragHandleContainer = (_columnDomRef$parentE = columnDomRef.parentElement) === null || _columnDomRef$parentE === void 0 ? void 0 : _columnDomRef$parentE.querySelector(':scope > [data-blocks-drag-handle-container]'); return dragHandleContainer === null || dragHandleContainer === void 0 ? void 0 : dragHandleContainer.querySelector(DRAG_HANDLE_SELECTOR); }; const focusTrap = { initialFocus: undefined }; export const LayoutColumnMenu = /*#__PURE__*/React.memo(function LayoutColumnMenu({ api, editorView, mountTo, boundariesElement, scrollableElement }) { var _api$uiControlRegistr, _api$uiControlRegistr2; const { isLayoutColumnMenuOpen, layoutColumnMenuAnchorPos, openedViaKeyboard, selection } = useSharedPluginStateWithSelector(api, ['layout', 'selection'], states => { var _states$layoutState$i, _states$layoutState, _states$layoutState2, _states$layoutState$l, _states$layoutState3, _states$selectionStat; return { isLayoutColumnMenuOpen: (_states$layoutState$i = (_states$layoutState = states.layoutState) === null || _states$layoutState === void 0 ? void 0 : _states$layoutState.isLayoutColumnMenuOpen) !== null && _states$layoutState$i !== void 0 ? _states$layoutState$i : false, layoutColumnMenuAnchorPos: (_states$layoutState2 = states.layoutState) === null || _states$layoutState2 === void 0 ? void 0 : _states$layoutState2.layoutColumnMenuAnchorPos, openedViaKeyboard: (_states$layoutState$l = (_states$layoutState3 = states.layoutState) === null || _states$layoutState3 === void 0 ? void 0 : _states$layoutState3.layoutColumnMenuOpenedViaKeyboard) !== null && _states$layoutState$l !== void 0 ? _states$layoutState$l : false, selection: (_states$selectionStat = states.selectionState) === null || _states$selectionStat === void 0 ? void 0 : _states$selectionStat.selection }; }); const closeLayoutColumnMenu = useCallback(() => { var _api$core, _api$layout; api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(api === null || api === void 0 ? void 0 : (_api$layout = api.layout) === null || _api$layout === void 0 ? void 0 : _api$layout.commands.toggleLayoutColumnMenu({ isOpen: false })); }, [api]); const handleClickOutside = useCallback(event => { if (event.target instanceof Element && (event.target.closest(TOOLBAR_MENU_SELECTOR) || event.target.closest(NESTED_DROPDOWN_MENU_SELECTOR))) { return; } // Clicking a drag handle should let the drag handle's own click handler // update selection/menu state. Treating it as a generic outside click // races that transaction and can immediately close the layout column menu. if (event.target instanceof Element && event.target.closest(DRAG_HANDLE_SELECTOR)) { return; } closeLayoutColumnMenu(); }, [closeLayoutColumnMenu]); const handleSetIsOpen = useCallback(isOpen => { if (!isOpen) { closeLayoutColumnMenu(); } }, [closeLayoutColumnMenu]); const handleArrowKeyNavigationClose = useCallback(event => { event.preventDefault(); closeLayoutColumnMenu(); }, [closeLayoutColumnMenu]); const shouldDisableArrowKeyNavigation = useCallback(event => { if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { return false; } const target = event.target; if (!(target instanceof HTMLElement)) { return false; } return target.closest(NESTED_DROPDOWN_MENU_SELECTOR) !== null; }, []); const menuWrapperRef = useRef(null); const handleMenuKeyDown = useCallback(event => { // Keep menu keyboard events scoped to the menu while preserving Escape and // ArrowUp/ArrowDown handling from Popup and ArrowKeyNavigationProvider. if (event.key !== 'Escape' && event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { event.stopPropagation(); } }, []); useEffect(() => { const menuWrapper = menuWrapperRef.current; if (!isLayoutColumnMenuOpen || !menuWrapper) { return; } return bind(menuWrapper, { type: 'keydown', listener: handleMenuKeyDown }); }, [handleMenuKeyDown, isLayoutColumnMenuOpen]); const components = (_api$uiControlRegistr = api === null || api === void 0 ? void 0 : (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(LAYOUT_COLUMN_MENU.key)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : []; const target = useMemo(() => isLayoutColumnMenuOpen ? getLayoutColumnMenuTarget(editorView, selection, layoutColumnMenuAnchorPos) : null, [editorView, isLayoutColumnMenuOpen, layoutColumnMenuAnchorPos, selection]); const hasValidTarget = target instanceof HTMLElement; const positionLayoutColumnMenu = useCallback(position => { var _menuWrapperRef$curre; const popup = (_menuWrapperRef$curre = menuWrapperRef.current) === null || _menuWrapperRef$curre === void 0 ? void 0 : _menuWrapperRef$curre.parentElement; if (!(target instanceof HTMLElement) || !(popup instanceof HTMLElement)) { return position; } if (shouldOpenLayoutColumnMenuBelow({ editorView, popup, scrollableElement, target })) { return calculateFallbackBottomPosition(position, target, popup); } return position; }, [editorView, scrollableElement, target]); useEffect(() => { if (isLayoutColumnMenuOpen && (!hasValidTarget || components.length === 0)) { closeLayoutColumnMenu(); } }, [closeLayoutColumnMenu, components.length, hasValidTarget, isLayoutColumnMenuOpen]); const { alignX, alignY, offset, useManualBelowFlip } = useMemo(() => getLayoutColumnMenuPositioningProps(), []); if (!isLayoutColumnMenuOpen || components.length === 0 || !hasValidTarget) { return null; } return /*#__PURE__*/React.createElement(PopupWithListeners, { target: target, mountTo: mountTo, boundariesElement: boundariesElement, scrollableElement: scrollableElement, zIndex: akEditorFloatingOverlapPanelZIndex, alignX: alignX, alignY: alignY, forcePlacement: true, preventOverflow: true, stick: true, offset: offset, onPositionCalculated: useManualBelowFlip ? positionLayoutColumnMenu : undefined, handleClickOutside: handleClickOutside, handleEscapeKeydown: closeLayoutColumnMenu, focusTrap: openedViaKeyboard ? focusTrap : undefined }, /*#__PURE__*/React.createElement("div", { ref: menuWrapperRef }, /*#__PURE__*/React.createElement(UserIntentPopupWrapper, { api: api, userIntent: "layoutColumnMenuPopupOpen" }, /*#__PURE__*/React.createElement(ToolbarDropdownMenuProvider, { isOpen: isLayoutColumnMenuOpen, setIsOpen: handleSetIsOpen }, /*#__PURE__*/React.createElement(ArrowKeyNavigationProvider, { type: ArrowKeyNavigationType.MENU, handleClose: handleArrowKeyNavigationClose, disableArrowKeyNavigation: shouldDisableArrowKeyNavigation }, /*#__PURE__*/React.createElement(SurfaceRenderer, { components: components, fallbacks: LAYOUT_COLUMN_MENU_FALLBACKS, surface: LAYOUT_COLUMN_MENU })))))); });