UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

304 lines (301 loc) 16.7 kB
/* block-controls-surface-drag-handle.tsx generated by @compiled/babel-plugin v3.0.2 */ import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; import "./block-controls-surface-drag-handle.compiled.css"; import * as React from 'react'; import { ax, ix } from "@compiled/react/runtime"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; // oxlint-disable-next-line typescript/consistent-type-imports -- jsx is the runtime factory for the classic JSX pragma above. import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics'; import { BLOCK_CONTROL_UI_CONTEXT } from '@atlaskit/editor-common/block-controls/block-control-ui-context'; import { surfaceDragHandleElementStore } from '@atlaskit/editor-common/block-controls/surface-drag-handle-element'; import { getBrowserInfo } from '@atlaskit/editor-common/browser'; import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { TooltipContentWithMultipleShortcuts } from '@atlaskit/editor-common/keymaps'; import { blockControlsMessages } from '@atlaskit/editor-common/messages'; import DragHandleVerticalIcon from '@atlaskit/icon/core/drag-handle-vertical'; import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled'; import { fg } from '@atlaskit/platform-feature-flags/fg'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import Tooltip from '@atlaskit/tooltip/Tooltip'; import { selectionPreservationPluginKey } from '../pm-plugins/selection-preservation/plugin-key'; import { prepareCollapsedHeadingSelection } from '../pm-plugins/utils/collapsed-heading'; import { expandAndUpdateSelection } from '../pm-plugins/utils/expand-and-update-selection'; import { isHandleCorrelatedToSelection } from '../pm-plugins/utils/getSelection'; import { buildLayoutColumnMenuMeta } from './block-controls-surface-drag-handle-utils'; import { DRAG_HANDLE_MAX_SHIFT_CLICK_DEPTH } from './consts'; import { DragHandleNestedIcon } from './drag-handle-nested-icon'; import { shouldUseNestedDragHandleIcon } from './should-use-nested-drag-handle-icon'; import { useSurfaceEditorView } from './surface-editor-view-context'; import { useBlockControlsSurfaceDragSource } from './use-block-controls-surface-drag-source'; import { getAnchorAttrName, NODE_ANCHOR_ATTR_NAME } from './utils/dom-attr-name'; // Keep style values co-located for Compiled while matching the legacy drag handle dimensions. var SURFACE_DRAG_HANDLE_HEIGHT = 24; var SURFACE_DRAG_HANDLE_WIDTH = 12; var SURFACE_DRAG_HANDLE_BORDER_RADIUS = 4; var SURFACE_DRAG_HANDLE_Z_INDEX = 100; var dragHandleStyles = { root: "_19itglyw _2rko1y44 _12ji1r31 _1qu2glyw _12y31o36 _v564k6bl _1e0c1txw _vchhusvi _2lx21bp4 _4cvr1h6o _1bah1h6o _4t3i1tcg _1bsb1crf _bfhksm61 _syazdgkc _80om1qgj _1pbyb4wl _ca0qze3t _u5f3ze3t _n3tdze3t _19bvze3t _cun91j28 _vbqb2sac _1ygbdfik _irr31dpa _6g80187o _1ggl5xtf _1di6fcek", scaled: "_4t3i1myc _1bsbx5ub", selected: "_bfhk15s3 _syazxl6c", keyboardFocused: "_1a37dfik", firefoxSelection: "_1p1y1j28", layoutColumn: "_t9ec14w8", iconWrapper: "_1e0c1txw _4cvr1h6o _1bah1h6o" }; var handleIconDragStart = function handleIconDragStart(event) { var _event$currentTarget$; if (!getBrowserInfo().chrome) { return; } event.stopPropagation(); (_event$currentTarget$ = event.currentTarget.parentElement) === null || _event$currentTarget$ === void 0 || _event$currentTarget$.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: event.dataTransfer })); }; var isSelected = function isSelected(selection, pos, view) { if (typeof pos !== 'number' || !selection || !view) { return false; } return isHandleCorrelatedToSelection(view.state, selection, pos); }; export var BlockControlsSurfaceDragHandle = function BlockControlsSurfaceDragHandle(_ref) { var _activeNode$type$name; var api = _ref.api, surfaceContext = _ref.surfaceContext; var _useIntl = useIntl(), formatMessage = _useIntl.formatMessage; var view = useSurfaceEditorView(); var context = surfaceContext === null || surfaceContext === void 0 ? void 0 : surfaceContext.get(BLOCK_CONTROL_UI_CONTEXT); var activeNode = context === null || context === void 0 ? void 0 : context.activeNode; var pos = activeNode === null || activeNode === void 0 ? void 0 : activeNode.pos; var nodeType = (_activeNode$type$name = activeNode === null || activeNode === void 0 ? void 0 : activeNode.type.name) !== null && _activeNode$type$name !== void 0 ? _activeNode$type$name : ''; var buttonRef = useRef(null); var posRef = useRef(pos); posRef.current = pos; var getPos = useCallback(function () { return posRef.current; }, []); var _useSharedPluginState = useSharedPluginStateWithSelector(api, ['blockControls', 'interaction', 'selection'], function (states) { var _states$blockControls, _states$blockControls2, _states$interactionSt, _states$blockControls3, _states$selectionStat; return { activeNodeAnchorName: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 || (_states$blockControls = _states$blockControls.activeNode) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.anchorName, handleOptions: (_states$blockControls2 = states.blockControlsState) === null || _states$blockControls2 === void 0 || (_states$blockControls2 = _states$blockControls2.activeNode) === null || _states$blockControls2 === void 0 ? void 0 : _states$blockControls2.handleOptions, interactionState: (_states$interactionSt = states.interactionState) === null || _states$interactionSt === void 0 ? void 0 : _states$interactionSt.interactionState, isShiftDown: (_states$blockControls3 = states.blockControlsState) === null || _states$blockControls3 === void 0 ? void 0 : _states$blockControls3.isShiftDown, selection: (_states$selectionStat = states.selectionState) === null || _states$selectionStat === void 0 ? void 0 : _states$selectionStat.selection }; }), activeNodeAnchorName = _useSharedPluginState.activeNodeAnchorName, handleOptions = _useSharedPluginState.handleOptions, interactionState = _useSharedPluginState.interactionState, isShiftDown = _useSharedPluginState.isShiftDown, selection = _useSharedPluginState.selection; var _useState = useState(false), _useState2 = _slicedToArray(_useState, 2), dragHandleSelected = _useState2[0], setDragHandleSelected = _useState2[1]; useEffect(function () { setDragHandleSelected(isSelected(selection, pos, view)); }, [pos, selection, view]); var isLayoutColumn = nodeType === 'layoutColumn'; var isTopLevelNode = (activeNode === null || activeNode === void 0 ? void 0 : activeNode.parentType) === 'doc'; var isDisabled = useMemo(function () { var _api$blockControls$sh; if (!isShiftDown || !view || view.state.selection.empty || !fg('platform_editor_elements_dnd_shift_click_select')) { return false; } if (isLayoutColumn && expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) { return false; } var multiSelect = api === null || api === void 0 || (_api$blockControls$sh = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh === void 0 ? void 0 : _api$blockControls$sh.multiSelectDnD; var $anchor = (multiSelect === null || multiSelect === void 0 ? void 0 : multiSelect.anchor) !== undefined ? view.state.doc.resolve(multiSelect.anchor) : view.state.selection.$anchor; return !isTopLevelNode || $anchor.depth > DRAG_HANDLE_MAX_SHIFT_CLICK_DEPTH; }, [api, isLayoutColumn, isShiftDown, isTopLevelNode, view]); var getAnchorName = useCallback(function () { var _api$blockControls$sh2, _ref2, _ref3, _dom$getAttribute, _dom$closest; var surfaceAnchor = posRef.current === undefined ? undefined : api === null || api === void 0 || (_api$blockControls$sh2 = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh2 === void 0 || (_api$blockControls$sh2 = _api$blockControls$sh2.surfaceAnchors) === null || _api$blockControls$sh2 === void 0 ? void 0 : _api$blockControls$sh2.get(posRef.current); if (surfaceAnchor) { return surfaceAnchor; } if (activeNodeAnchorName) { return activeNodeAnchorName; } var currentPos = posRef.current; if (!view || currentPos === undefined) { return ''; } var dom = view.nodeDOM(currentPos); if (!(dom instanceof HTMLElement)) { return ''; } var anchorAttrName = getAnchorAttrName(); return (_ref2 = (_ref3 = (_dom$getAttribute = dom.getAttribute(anchorAttrName)) !== null && _dom$getAttribute !== void 0 ? _dom$getAttribute : dom.getAttribute(NODE_ANCHOR_ATTR_NAME)) !== null && _ref3 !== void 0 ? _ref3 : (_dom$closest = dom.closest("[".concat(anchorAttrName, "]"))) === null || _dom$closest === void 0 ? void 0 : _dom$closest.getAttribute(anchorAttrName)) !== null && _ref2 !== void 0 ? _ref2 : ''; }, [activeNodeAnchorName, api, view]); useLayoutEffect(function () { var element = buttonRef.current; if (!view || !element) { return; } surfaceDragHandleElementStore.set(view, element); return function () { return surfaceDragHandleElementStore.release(view, element); }; }, [view]); useEffect(function () { if (!(handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused) || !buttonRef.current || !view) { return; } var animationFrameId; var attempts = 0; var _focusHandle = function focusHandle() { var button = buttonRef.current; if (!button) { return; } button.focus(); if (button.ownerDocument.activeElement !== button && attempts < 5) { attempts++; animationFrameId = requestAnimationFrame(_focusHandle); } }; _focusHandle(); return function () { if (animationFrameId !== undefined) { cancelAnimationFrame(animationFrameId); } view.focus(); }; }, [handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view]); useBlockControlsSurfaceDragSource({ api: api, elementRef: buttonRef, getAnchorName: getAnchorName, getPos: getPos, nodeType: nodeType, start: pos, view: view }); var handleMouseDown = useCallback(function () { var _api$core, _api$blockControls; api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(api === null || api === void 0 || (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.commands.setSelectedViaDragHandle(true)); }, [api]); var handleClick = useCallback(function (event) { var _api$core2; if (!view || isDisabled) { return; } var openedViaKeyboard = event.detail === 0; api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref4) { var _selectionPreservatio, _selectionPreservatio2, _api$analytics, _resolvedStartPos$nod, _api$blockControls2, _api$blockControls3, _activeNode$rootPos; var tr = _ref4.tr; var startPos = getPos(); if (startPos === undefined) { return tr; } if (nodeType === 'layoutColumn' && expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) { tr.setMeta('toggleLayoutColumnMenu', buildLayoutColumnMenuMeta(startPos, openedViaKeyboard)); } var selection = (_selectionPreservatio = (_selectionPreservatio2 = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio2 === void 0 ? void 0 : _selectionPreservatio2.preservedSelection) !== null && _selectionPreservatio !== void 0 ? _selectionPreservatio : tr.selection; var resolvedStartPos = tr.doc.resolve(startPos); api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.attachAnalyticsEvent({ action: ACTION.CLICKED, actionSubject: ACTION_SUBJECT.BUTTON, actionSubjectId: ACTION_SUBJECT_ID.ELEMENT_DRAG_HANDLE, attributes: { nodeDepth: resolvedStartPos.depth, nodeTypes: ((_resolvedStartPos$nod = resolvedStartPos.nodeAfter) === null || _resolvedStartPos$nod === void 0 ? void 0 : _resolvedStartPos$nod.type.name) || '' }, eventType: EVENT_TYPE.UI })(tr); expandAndUpdateSelection({ api: api, isShiftPressed: event.shiftKey, nodeType: nodeType, selection: selection, startPos: startPos, tr: tr }); if (isExperimentEnabled('platform_editor_collapsible_headings')) { prepareCollapsedHeadingSelection({ api: api, expand: true, headingPos: startPos, tr: tr }); } api === null || api === void 0 || (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 || _api$blockControls2.commands.startPreservingSelection()({ tr: tr }); api === null || api === void 0 || (_api$blockControls3 = api.blockControls) === null || _api$blockControls3 === void 0 || _api$blockControls3.commands.toggleBlockMenu({ anchorName: getAnchorName(), openedViaKeyboard: openedViaKeyboard, triggerByNode: { nodeType: nodeType, pos: startPos, rootPos: (_activeNode$rootPos = activeNode === null || activeNode === void 0 ? void 0 : activeNode.rootPos) !== null && _activeNode$rootPos !== void 0 ? _activeNode$rootPos : resolvedStartPos.before(1) } })({ tr: tr }); tr.setMeta('scrollIntoView', false); return tr; }); view.focus(); }, [activeNode === null || activeNode === void 0 ? void 0 : activeNode.rootPos, api, getAnchorName, getPos, isDisabled, nodeType, view]); var label = formatMessage(blockControlsMessages.dragToMoveClickToOpen, { br: ' ' }); var helpDescriptors = useMemo(function () { return [{ description: formatMessage(blockControlsMessages.dragToMoveClickToOpen, { br: /*#__PURE__*/React.createElement("br", null) }) }]; }, [formatMessage]); var handleTooltipShow = useCallback(function () { var _api$accessibilityUti; api === null || api === void 0 || (_api$accessibilityUti = api.accessibilityUtils) === null || _api$accessibilityUti === void 0 || _api$accessibilityUti.actions.ariaNotify(label, { priority: 'important' }); }, [api, label]); if (!view || !activeNode || pos === undefined) { return null; } var tooltipContent = /*#__PURE__*/React.createElement(TooltipContentWithMultipleShortcuts, { helpDescriptors: helpDescriptors }); var browser = getBrowserInfo(); var button = /*#__PURE__*/React.createElement("button", { ref: buttonRef, type: "button", "aria-label": label, disabled: isDisabled, onClick: handleClick, onMouseDown: handleMouseDown // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop -- Pragmatic DnD owns this native drag source. , onDrop: function onDrop(event) { return event.stopPropagation(); }, "data-editor-block-ctrl-drag-handle": true, "data-blocks-drag-handle": fg('confluence_remix_button_right_side_block_fg') || undefined, "data-testid": "block-controls-surface-drag-handle", className: ax([dragHandleStyles.root, browser.gecko && dragHandleStyles.firefoxSelection, isLayoutColumn && dragHandleStyles.layoutColumn, dragHandleSelected && interactionState !== 'hasNotHadInteraction' && dragHandleStyles.selected, (handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused) && dragHandleStyles.keyboardFocused, dragHandleStyles.scaled]) }, /*#__PURE__*/React.createElement("span", { onDragStart: handleIconDragStart, className: ax([dragHandleStyles.iconWrapper]) }, shouldUseNestedDragHandleIcon(Boolean(isTopLevelNode), isLayoutColumn) ? /*#__PURE__*/React.createElement(DragHandleNestedIcon, null) : /*#__PURE__*/React.createElement(DragHandleVerticalIcon, { label: "", size: "small", spacing: "spacious" }))); return /*#__PURE__*/React.createElement(Tooltip, { position: "top", content: tooltipContent, ignoreTooltipPointerEvents: true, onShow: handleTooltipShow }, button); };