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