@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
1,214 lines • 64.7 kB
JavaScript
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
/**
* @jsxRuntime classic
* @jsx jsx
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports
import { css, jsx } from '@emotion/react';
import { bind } from 'bind-event-listener';
import { getDocument } from '@atlaskit/browser-apis';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { getBrowserInfo } from '@atlaskit/editor-common/browser';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
import { dragToMoveDown, dragToMoveLeft, dragToMoveRight, dragToMoveUp, getAriaKeyshortcuts, TooltipContentWithMultipleShortcuts } from '@atlaskit/editor-common/keymaps';
import { blockControlsMessages } from '@atlaskit/editor-common/messages';
import { DRAG_HANDLE_WIDTH, tableControlsSpacing } from '@atlaskit/editor-common/styles';
import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
import { akEditorFullPageNarrowBreakout, akEditorTableToolbarSize, relativeSizeToBaseFontSize } from '@atlaskit/editor-shared-styles/consts';
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 { draggable } from '@atlaskit/pragmatic-drag-and-drop/adapter/element-adapter';
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/utils/set-custom-native-drag-preview';
// eslint-disable-next-line @atlaskit/design-system/no-emotion-primitives -- to be migrated to @atlaskit/primitives/compiled – go/akcss
import { Box, xcss } from '@atlaskit/primitives';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/editor-experiment';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
import Tooltip from '@atlaskit/tooltip/Tooltip';
import { getNodeTypeWithLevel } from '../pm-plugins/decorations-common';
import { key } from '../pm-plugins/main';
import { selectionPreservationPluginKey } from '../pm-plugins/selection-preservation/plugin-key';
import { getMultiSelectAnalyticsAttributes } from '../pm-plugins/utils/analytics';
import { getControlBottomCSSValue, getControlHeightCSSValue, getLeftPosition, getNodeHeight, getTopPosition, shouldBeSticky, shouldMaskNodeControls } from '../pm-plugins/utils/drag-handle-positions';
import { expandAndUpdateSelection } from '../pm-plugins/utils/expand-and-update-selection';
import { isHandleCorrelatedToSelection, selectNode } from '../pm-plugins/utils/getSelection';
import { ACTIVE_DRAG_HANDLE_ATTR, ACTIVE_DRAG_HANDLE_FALLBACK_ANCHOR_NAME, DRAG_HANDLE_BORDER_RADIUS, DRAG_HANDLE_HEIGHT, DRAG_HANDLE_MAX_SHIFT_CLICK_DEPTH, DRAG_HANDLE_ZINDEX, dragHandleGap, nodeMargins, spacingBetweenNodesForPreview, STICKY_CONTROLS_TOP_MARGIN, STICKY_CONTROLS_TOP_MARGIN_FOR_STICKY_HEADER, topPositionAdjustment } from './consts';
import { DragHandleNestedIcon } from './drag-handle-nested-icon';
import { dragPreview } from './drag-preview';
import { shouldUseNestedDragHandleIcon } from './should-use-nested-drag-handle-icon';
import { refreshAnchorName } from './utils/anchor-name';
import { getAnchorAttrName } from './utils/dom-attr-name';
import { VisibilityContainer } from './visibility-container';
var iconWrapperStyles = xcss({
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
});
var buttonWrapperStylesNoBackground = css({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-drag-handle-container]:has(+ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
marginBottom: "var(--ds-space-negative-200, -16px)",
paddingBottom: "var(--ds-space-200, 16px)",
marginTop: "var(--ds-space-negative-400, -32px)",
paddingTop: "calc(".concat("var(--ds-space-400, 32px)", " - 1px)"),
marginRight: "var(--ds-space-negative-150, -12px)",
paddingRight: "var(--ds-space-150, 12px)",
boxSizing: 'border-box'
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-drag-handle-container]):has(+ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
marginBottom: "var(--ds-space-negative-200, -16px)",
paddingBottom: "var(--ds-space-200, 16px)",
marginTop: "var(--ds-space-negative-400, -32px)",
paddingTop: "calc(".concat("var(--ds-space-400, 32px)", " - 1px)"),
marginRight: "var(--ds-space-negative-150, -12px)",
paddingRight: "var(--ds-space-150, 12px)",
boxSizing: 'border-box'
}
});
var buttonWrapperStylesPatch = css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-drag-handle-container]:has(+ [data-prosemirror-node-name="table"] .pm-table-with-controls [data-number-column="true"] tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
marginRight: -akEditorTableToolbarSize,
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
paddingRight: akEditorTableToolbarSize
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-drag-handle-container]):has(+ [data-prosemirror-node-name="table"] .pm-table-with-controls [data-number-column="true"] tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
marginRight: -akEditorTableToolbarSize,
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
paddingRight: akEditorTableToolbarSize
}
});
// update color to match quick insert button for new editor controls
var dragHandleColor = css({
color: "var(--ds-icon-subtle, #505258)"
});
var dragHandleButtonStyles = css({
display: 'flex',
boxSizing: 'border-box',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
height: DRAG_HANDLE_HEIGHT,
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
width: DRAG_HANDLE_WIDTH,
border: 'none',
background: 'transparent',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
borderRadius: DRAG_HANDLE_BORDER_RADIUS,
// when platform_editor_controls is enabled, the drag handle color is overridden. Update color here when experiment is cleaned up.
color: "var(--ds-icon, #292A2E)",
cursor: 'grab',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
zIndex: DRAG_HANDLE_ZINDEX,
outline: 'none',
'&:hover': {
backgroundColor: "var(--ds-background-neutral-subtle-hovered, #0515240F)"
},
'&:active': {
backgroundColor: "var(--ds-background-neutral-subtle-pressed, #0B120E24)"
},
'&:disabled': {
color: "var(--ds-icon-disabled, #080F214A)",
backgroundColor: 'transparent'
},
'&:hover:disabled': {
backgroundColor: "var(--ds-background-disabled, #0515240F)"
}
});
// Calculate scaled dimensions based on the base font size using CSS calc()
// Default font size is 16px, scale proportionally
// Standard: 16px -> 24h x 12w, Dense: 13px -> 18h x 9w, Jira: 14px -> 21h x 12w
var dragHandleButtonScaledStyles = css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
height: relativeSizeToBaseFontSize(DRAG_HANDLE_HEIGHT),
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
width: relativeSizeToBaseFontSize(DRAG_HANDLE_WIDTH)
});
var dragHandleButtonSmallScreenStyles = css(_defineProperty({}, "@container editor-area (max-width: ".concat(akEditorFullPageNarrowBreakout, "px)"), {
opacity: 0,
visibility: 'hidden'
}));
var dragHandleButtonStylesOld = css({
position: 'absolute',
paddingTop: "var(--ds-space-025, 2px)",
paddingBottom: "var(--ds-space-025, 2px)",
paddingLeft: '0',
paddingRight: '0',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
height: DRAG_HANDLE_HEIGHT,
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
width: DRAG_HANDLE_WIDTH,
border: 'none',
background: 'transparent',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
borderRadius: DRAG_HANDLE_BORDER_RADIUS,
// when platform_editor_controls is enabled, the drag handle color is overridden. Update color here when experiment is cleaned up.
color: "var(--ds-icon, #292A2E)",
cursor: 'grab',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
zIndex: DRAG_HANDLE_ZINDEX,
outline: 'none',
'&:hover': {
backgroundColor: "var(--ds-background-neutral-subtle-hovered, #0515240F)"
},
'&:active': {
backgroundColor: "var(--ds-background-neutral-subtle-pressed, #0B120E24)"
},
'&:focus': {
outline: "var(--ds-border-width-focused, 2px)".concat(" solid ", "var(--ds-border-focused, #4688EC)")
},
'&:disabled': {
color: "var(--ds-icon-disabled, #080F214A)",
backgroundColor: 'transparent'
},
'&:hover:disabled': {
backgroundColor: "var(--ds-background-disabled, #0515240F)"
}
});
var focusedStyles = css({
'&:focus-visible': {
outline: "var(--ds-border-width-focused, 2px)".concat(" solid ", "var(--ds-border-focused, #4688EC)")
}
});
var keyboardFocusedDragHandleStyles = css({
outline: "var(--ds-border-width-focused, 2px)".concat(" solid ", "var(--ds-border-focused, #4688EC)")
});
var dragHandleContainerStyles = xcss({
position: 'absolute',
boxSizing: 'border-box'
});
var tooltipContainerStyles = css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
bottom: "-".concat(STICKY_CONTROLS_TOP_MARGIN, "px"),
position: 'sticky',
display: 'block',
zIndex: 100 // card = 100
});
var tooltipContainerStylesStickyHeaderWithMask = css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: "".concat(STICKY_CONTROLS_TOP_MARGIN, "px"),
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-drag-handle-container]:has(+ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
top: '0'
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-drag-handle-container]):has(+ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
top: '0'
}
});
var tooltipContainerStylesImprovedStickyHeaderWithMask = css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: "".concat(STICKY_CONTROLS_TOP_MARGIN, "px"),
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-drag-handle-container]:has(+ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
top: '0'
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-drag-handle-container]):has(+ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
top: '0'
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-drag-handle-container]:has(+ [data-prosemirror-mark-name="fragment"] >[data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: tableControlsSpacing
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-drag-handle-container]):has(+ [data-prosemirror-mark-name="fragment"] >[data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: tableControlsSpacing
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-drag-handle-container]:has(+ [data-prosemirror-node-name="table"] tr.pm-table-row-native-sticky.pm-table-row-native-sticky-active) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: "".concat(STICKY_CONTROLS_TOP_MARGIN_FOR_STICKY_HEADER, "px")
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-drag-handle-container]):has(+ [data-prosemirror-node-name="table"] tr.pm-table-row-native-sticky.pm-table-row-native-sticky-active) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: "".concat(STICKY_CONTROLS_TOP_MARGIN_FOR_STICKY_HEADER, "px")
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-drag-handle-container]:has(+ [data-prosemirror-mark-name="fragment"] >[data-prosemirror-node-name="table"] tr.pm-table-row-native-sticky.pm-table-row-native-sticky-active) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: "".concat(STICKY_CONTROLS_TOP_MARGIN_FOR_STICKY_HEADER, "px")
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-drag-handle-container]):has(+ [data-prosemirror-mark-name="fragment"] > [data-prosemirror-node-name="table"] tr.pm-table-row-native-sticky.pm-table-row-native-sticky-active) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: "".concat(STICKY_CONTROLS_TOP_MARGIN_FOR_STICKY_HEADER, "px")
}
});
var tooltipContainerStylesStickyHeaderWithoutMask = css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: "".concat(STICKY_CONTROLS_TOP_MARGIN, "px"),
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-drag-handle-container]:has(+ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: tableControlsSpacing
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-drag-handle-container]):has(+ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: tableControlsSpacing
}
});
var dragHandleMultiLineSelectionFixFirefox = css({
'&::selection': {
backgroundColor: 'transparent'
}
});
var layoutColumnDragHandleStyles = css({
transform: 'rotate(90deg)'
});
var selectedStyles = css({
backgroundColor: "var(--ds-background-selected, #E9F2FE)",
color: "var(--ds-icon-selected, #1868DB)"
});
// [Chrome only] When selection contains multiple nodes and then drag a drag handle that is within the selection range,
// icon span receives dragStart event, instead of button, and since it is not registered as a draggable element
// with pragmatic DnD and pragmatic DnD is not triggered
var handleIconDragStart = function handleIconDragStart(e) {
var browser = getBrowserInfo();
if (!browser.chrome) {
return;
}
// prevent dragStart handler triggered by icon
e.stopPropagation();
var dragEvent = new DragEvent('dragstart', {
bubbles: true,
cancelable: true,
dataTransfer: e.dataTransfer
});
if (e.target instanceof HTMLElement) {
var _e$target$closest;
// re-dispatch drag event on button so that pragmatic DnD can be triggered properly
(_e$target$closest = e.target.closest('button')) === null || _e$target$closest === void 0 || _e$target$closest.dispatchEvent(dragEvent);
}
};
var getNodeSpacingForPreview = function getNodeSpacingForPreview(node) {
if (!node) {
return spacingBetweenNodesForPreview['default'];
}
var nodeTypeName = node.type.name;
if (nodeTypeName === 'heading') {
return spacingBetweenNodesForPreview["heading".concat(node.attrs.level)] || spacingBetweenNodesForPreview['default'];
}
return spacingBetweenNodesForPreview[nodeTypeName] || spacingBetweenNodesForPreview['default'];
};
var getNodeMargins = function getNodeMargins(node) {
if (!node) {
return nodeMargins['default'];
}
var nodeTypeName = node.type.name;
if (nodeTypeName === 'heading') {
return nodeMargins["heading".concat(node.attrs.level)] || nodeMargins['default'];
}
return nodeMargins[nodeTypeName] || nodeMargins['default'];
};
// Kill switch OFF: omit `isOpen` so the reducer toggles per clicked column. ON: keep
// `isOpen: true` for legacy always-open. Centralised so all dispatch sites stay in sync.
var buildToggleLayoutColumnMenuMeta = function buildToggleLayoutColumnMenuMeta(anchorPos, openedViaKeyboard) {
return _objectSpread(_objectSpread({
anchorPos: anchorPos
}, fg('platform_editor_layout_column_menu_kill_switch_1') ? {
isOpen: true
} : {}), {}, {
openedViaKeyboard: openedViaKeyboard
});
};
var getDragHandleAnchorReference = function getDragHandleAnchorReference(_ref) {
var edge = _ref.edge,
safeAnchorName = _ref.safeAnchorName;
if (expValEquals('platform_editor_controls_reliable_anchor', 'isEnabled', true)) {
return "anchor(".concat(safeAnchorName, " ").concat(edge, ", anchor(").concat(ACTIVE_DRAG_HANDLE_FALLBACK_ANCHOR_NAME, " ").concat(edge, "))");
}
return "anchor(".concat(safeAnchorName, " ").concat(edge, ")");
};
export var DragHandle = function DragHandle(_ref2) {
var _api$blockControls7;
var view = _ref2.view,
api = _ref2.api,
formatMessage = _ref2.formatMessage,
getPos = _ref2.getPos,
anchorName = _ref2.anchorName,
nodeType = _ref2.nodeType,
handleOptions = _ref2.handleOptions,
anchorRectCache = _ref2.anchorRectCache;
var buttonRef = useRef(null);
var mouseDownRef = useRef(false);
var _useState = useState(false),
_useState2 = _slicedToArray(_useState, 2),
dragHandleSelected = _useState2[0],
setDragHandleSelected = _useState2[1];
var _useState3 = useState(false),
_useState4 = _slicedToArray(_useState3, 2),
dragHandleDisabled = _useState4[0],
setDragHandleDisabled = _useState4[1];
var _useState5 = useState(768),
_useState6 = _slicedToArray(_useState5, 2),
blockCardWidth = _useState6[0],
setBlockCardWidth = _useState6[1];
var _useState7 = useState({
display: 'none'
}),
_useState8 = _slicedToArray(_useState7, 2),
positionStylesOld = _useState8[0],
setPositionStylesOld = _useState8[1];
// Tracks whether the initial position calculation has been performed at least once.
// The reliable-anchor early-return optimisation must not fire before the first calculation,
// otherwise positionStylesOld stays as { display: 'none' } and the handle is never shown.
var hasCalculatedInitialPosition = useRef(false);
var _useState9 = useState(Boolean(handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused)),
_useState0 = _slicedToArray(_useState9, 2),
isFocused = _useState0[0],
setIsFocused = _useState0[1];
var _useSharedPluginState = useSharedPluginStateWithSelector(api, ['featureFlags', 'selection', 'blockControls', 'interaction', 'userIntent'], function (states) {
var _states$featureFlagsS, _states$selectionStat, _states$blockControls, _states$interactionSt, _states$userIntentSta;
return {
macroInteractionUpdates: (_states$featureFlagsS = states.featureFlagsState) === null || _states$featureFlagsS === void 0 ? void 0 : _states$featureFlagsS.macroInteractionUpdates,
selection: (_states$selectionStat = states.selectionState) === null || _states$selectionStat === void 0 ? void 0 : _states$selectionStat.selection,
isShiftDown: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.isShiftDown,
interactionState: (_states$interactionSt = states.interactionState) === null || _states$interactionSt === void 0 ? void 0 : _states$interactionSt.interactionState,
currentUserIntent: (_states$userIntentSta = states.userIntentState) === null || _states$userIntentSta === void 0 ? void 0 : _states$userIntentSta.currentUserIntent
};
}),
macroInteractionUpdates = _useSharedPluginState.macroInteractionUpdates,
selection = _useSharedPluginState.selection,
isShiftDown = _useSharedPluginState.isShiftDown,
interactionState = _useSharedPluginState.interactionState,
currentUserIntent = _useSharedPluginState.currentUserIntent;
var start = getPos();
var isLayoutColumn = nodeType === 'layoutColumn';
// Dynamically calculate if node is top-level based on current position
var isTopLevelNodeValue = useMemo(function () {
var pos = getPos();
if (typeof pos === 'number') {
var $pos = view.state.doc.resolve(pos);
return ($pos === null || $pos === void 0 ? void 0 : $pos.parent.type.name) === 'doc';
}
return true;
}, [getPos, view.state.doc]);
useEffect(function () {
// blockCard/datasource width is rendered correctly after this decoraton does. We need to observe for changes.
if (nodeType === 'blockCard') {
var dom = view.dom.querySelector("[".concat(getAnchorAttrName(), "=\"").concat(anchorName, "\"]"));
var container = dom === null || dom === void 0 ? void 0 : dom.querySelector('.datasourceView-content-inner-wrap');
if (container) {
var resizeObserver = new ResizeObserver(function (entries) {
var width = entries[0].contentBoxSize[0].inlineSize;
setBlockCardWidth(width);
});
resizeObserver.observe(container);
return function () {
return resizeObserver.unobserve(container);
};
}
}
}, [anchorName, nodeType, view.dom]);
useEffect(function () {
if (!expValEqualsNoExposure('platform_editor_selection_toolbar_block_handle', 'isEnabled', true)) {
return;
}
var unbind = bind(window, {
type: 'mouseUp',
listener: function listener() {
return mouseDownRef.current = false;
}
});
return function () {
return unbind();
};
}, []);
var handleMouseDown = useCallback(function () {
mouseDownRef.current = true;
}, []);
var handleMouseUp = useCallback(function (e) {
// Stop propagation so that for drag handles in nested scenarios the click is captured
// and doesn't propagate to the edge of the element and trigger a node selection
// on the parent element
if (!expValEqualsNoExposure('platform_editor_selection_toolbar_block_handle', 'isEnabled', true)) {
e.stopPropagation();
}
// Fixes bug where selection toolbar is blocked when mouse is released on drag handle
if (mouseDownRef.current) {
e.stopPropagation();
}
}, []);
var handleOnClickNew = useCallback(function (e) {
var _api$core;
api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(function (_ref3) {
var _selectionPreservatio, _api$analytics, _resolvedStartPos$nod, _api$blockControls, _api$blockControls2;
var tr = _ref3.tr;
var startPos = getPos();
if (startPos === undefined) {
return tr;
}
if (nodeType === 'layoutColumn' && expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
tr.setMeta('toggleLayoutColumnMenu', buildToggleLayoutColumnMenuMeta(startPos, false));
}
var resolvedStartPos = tr.doc.resolve(startPos);
var selection = ((_selectionPreservatio = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection) || tr.selection;
api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.attachAnalyticsEvent({
eventType: EVENT_TYPE.UI,
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) || ''
}
})(tr);
expandAndUpdateSelection({
tr: tr,
selection: selection,
startPos: startPos,
isShiftPressed: e.shiftKey,
nodeType: nodeType,
api: api
});
api === null || api === void 0 || (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 || _api$blockControls.commands.startPreservingSelection()({
tr: tr
});
api === null || api === void 0 || (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 || _api$blockControls2.commands.toggleBlockMenu({
anchorName: anchorName,
openedViaKeyboard: false,
triggerByNode: {
nodeType: nodeType,
pos: startPos,
rootPos: tr.doc.resolve(startPos).before(1)
}
})({
tr: tr
});
tr.setMeta('scrollIntoView', false);
return tr;
});
view.focus();
}, [api, view, getPos, nodeType, anchorName]);
var handleKeyDownNew = useCallback(function (e) {
// allow user to use spacebar to select the node
if (e.key === 'Enter' || !e.repeat && e.key === ' ') {
var _getDocument, _api$core2;
if (((_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement) !== buttonRef.current) {
return;
}
e.preventDefault();
e.stopPropagation();
var startPos = getPos();
api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(function (_ref4) {
var _selectionPreservatio2, _api$blockControls3, _api$blockControls4, _api$userIntent;
var tr = _ref4.tr;
if (startPos === undefined) {
return tr;
}
var selection = ((_selectionPreservatio2 = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio2 === void 0 ? void 0 : _selectionPreservatio2.preservedSelection) || tr.selection;
expandAndUpdateSelection({
tr: tr,
selection: selection,
startPos: startPos,
isShiftPressed: e.shiftKey,
nodeType: nodeType,
api: api
});
api === null || api === void 0 || (_api$blockControls3 = api.blockControls) === null || _api$blockControls3 === void 0 || _api$blockControls3.commands.startPreservingSelection()({
tr: tr
});
if (nodeType === 'layoutColumn' && expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
tr.setMeta('toggleLayoutColumnMenu', buildToggleLayoutColumnMenuMeta(startPos, true));
}
var triggerByNode = {
nodeType: nodeType,
pos: startPos,
rootPos: tr.doc.resolve(startPos).before(1)
};
api === null || api === void 0 || (_api$blockControls4 = api.blockControls) === null || _api$blockControls4 === void 0 || _api$blockControls4.commands.toggleBlockMenu({
anchorName: anchorName,
triggerByNode: triggerByNode,
openedViaKeyboard: true
})({
tr: tr
});
api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 || _api$userIntent.commands.setCurrentUserIntent('blockMenuOpen')({
tr: tr
});
return tr;
});
view.focus();
} else if (![e.altKey, e.ctrlKey, e.shiftKey].some(function (pressed) {
return pressed;
})) {
// If not trying to press shortcut keys,
// return focus to editor to resume editing from caret position
view.focus();
}
}, [getPos, api, nodeType, anchorName, view]);
useEffect(function () {
var element = buttonRef.current;
if (!element) {
return;
}
return draggable({
element: element,
getInitialData: function getInitialData() {
return {
type: 'element',
start: start
};
},
onGenerateDragPreview: function onGenerateDragPreview(_ref5) {
var _api$core3, _api$blockControls$sh;
var nativeSetDragImage = _ref5.nativeSetDragImage;
api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(function (_ref6) {
var tr = _ref6.tr;
var handlePos = getPos();
if (typeof handlePos !== 'number') {
return tr;
}
var newHandlePosCheck = isHandleCorrelatedToSelection(view.state, tr.selection, handlePos);
if (!tr.selection.empty && newHandlePosCheck) {
var _api$blockControls5;
api === null || api === void 0 || (_api$blockControls5 = api.blockControls) === null || _api$blockControls5 === void 0 || _api$blockControls5.commands.setMultiSelectPositions()({
tr: tr
});
} else {
tr = selectNode(tr, handlePos, nodeType, api);
}
return tr;
});
var startPos = getPos();
var state = view.state;
var doc = state.doc,
selection = state.selection;
var sliceFrom = selection.from;
var sliceTo = selection.to;
var mSelect = api === null || api === void 0 || (_api$blockControls$sh = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh === void 0 ? void 0 : _api$blockControls$sh.multiSelectDnD;
if (mSelect) {
var anchor = mSelect.anchor,
head = mSelect.head;
sliceFrom = Math.min(anchor, head);
sliceTo = Math.max(anchor, head);
}
var expandedSlice = doc.slice(sliceFrom, sliceTo);
var isDraggingMultiLine = startPos !== undefined && startPos >= sliceFrom && startPos < sliceTo && expandedSlice.content.childCount > 1;
setCustomNativeDragPreview({
getOffset: function getOffset() {
if (!isDraggingMultiLine) {
return {
x: 0,
y: 0
};
} else {
// Calculate the offset of the preview container,
// So when drag multiple nodes, the preview align with the position of the selected nodes
var domAtPos = view.domAtPos.bind(view);
var domElementsHeightBeforeHandle = 0;
var nodesStartPos = [];
var nodesEndPos = [];
var activeNodeMarginTop = 0;
for (var i = 0; i < expandedSlice.content.childCount; i++) {
if (i === 0) {
var _expandedSlice$conten;
nodesStartPos[i] = sliceFrom;
nodesEndPos[i] = sliceFrom + (((_expandedSlice$conten = expandedSlice.content.maybeChild(i)) === null || _expandedSlice$conten === void 0 ? void 0 : _expandedSlice$conten.nodeSize) || 0);
} else {
var _expandedSlice$conten2;
nodesStartPos[i] = nodesEndPos[i - 1];
nodesEndPos[i] = nodesStartPos[i] + (((_expandedSlice$conten2 = expandedSlice.content.maybeChild(i)) === null || _expandedSlice$conten2 === void 0 ? void 0 : _expandedSlice$conten2.nodeSize) || 0);
}
// when the node is before the handle, calculate the height of the node
if (nodesEndPos[i] <= startPos) {
// eslint-disable-next-line @atlaskit/editor/no-as-casting
var currentNodeElement = findDomRefAtPos(nodesStartPos[i], domAtPos);
var maybeCurrentNode = expandedSlice.content.maybeChild(i);
var currentNodeSpacing = maybeCurrentNode ? getNodeMargins(maybeCurrentNode).top + getNodeMargins(maybeCurrentNode).bottom : 0;
domElementsHeightBeforeHandle = domElementsHeightBeforeHandle + currentNodeElement.offsetHeight + currentNodeSpacing;
} else {
// when the node is after the handle, calculate the top margin of the active node
var maybeNextNode = expandedSlice.content.maybeChild(i);
activeNodeMarginTop = maybeNextNode ? getNodeMargins(maybeNextNode).top : 0;
break;
}
}
return {
x: 0,
y: domElementsHeightBeforeHandle + activeNodeMarginTop
};
}
},
render: function render(_ref7) {
var container = _ref7.container;
var dom = view.dom.querySelector("[".concat(getAnchorAttrName(), "=\"").concat(anchorName, "\"]"));
if (!dom) {
return;
}
if (!isDraggingMultiLine) {
return dragPreview(container, {
dom: dom,
nodeType: nodeType
});
} else {
var domAtPos = view.domAtPos.bind(view);
var previewContent = [];
expandedSlice.content.descendants(function (node, pos) {
// Get the dom element of the node
//eslint-disable-next-line @atlaskit/editor/no-as-casting
var nodeDomElement = findDomRefAtPos(sliceFrom + pos, domAtPos);
var currentNodeSpacing = getNodeSpacingForPreview(node);
previewContent.push({
dom: nodeDomElement,
nodeType: node.type.name,
nodeSpacing: currentNodeSpacing
});
return false; // Only iterate through the first level of nodes
});
return dragPreview(container, previewContent);
}
},
nativeSetDragImage: nativeSetDragImage
});
},
onDragStart: function onDragStart() {
var _api$core4;
if (start === undefined) {
return;
}
api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(function (_ref8) {
var _api$blockControls$sh2, _api$blockControls6, _api$analytics2;
var tr = _ref8.tr;
var nodeTypes, hasSelectedMultipleNodes;
var resolvedMovingNode = tr.doc.resolve(start);
var maybeNode = resolvedMovingNode.nodeAfter;
var mSelect = api === null || api === void 0 || (_api$blockControls$sh2 = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh2 === void 0 ? void 0 : _api$blockControls$sh2.multiSelectDnD;
if (mSelect) {
var attributes = getMultiSelectAnalyticsAttributes(tr, mSelect.anchor, mSelect.head);
nodeTypes = attributes.nodeTypes;
hasSelectedMultipleNodes = attributes.hasSelectedMultipleNodes;
} else {
nodeTypes = maybeNode === null || maybeNode === void 0 ? void 0 : maybeNode.type.name;
hasSelectedMultipleNodes = false;
}
api === null || api === void 0 || (_api$blockControls6 = api.blockControls) === null || _api$blockControls6 === void 0 || _api$blockControls6.commands.setNodeDragged(getPos, anchorName, nodeType)({
tr: tr
});
tr.setMeta('scrollIntoView', false);
api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 || _api$analytics2.actions.attachAnalyticsEvent({
eventType: EVENT_TYPE.UI,
action: ACTION.DRAGGED,
actionSubject: ACTION_SUBJECT.ELEMENT,
actionSubjectId: ACTION_SUBJECT_ID.ELEMENT_DRAG_HANDLE,
attributes: {
nodeDepth: resolvedMovingNode.depth,
nodeTypes: nodeTypes || '',
hasSelectedMultipleNodes: hasSelectedMultipleNodes
}
})(tr);
return tr;
});
view.focus();
}
});
}, [anchorName, api, getPos, nodeType, start, view]);
var calculatePositionOld = useCallback(function () {
var _$pos$nodeAfter;
var pos = getPos();
var $pos = typeof pos === 'number' ? view.state.doc.resolve(pos) : undefined;
var parentPos = $pos && $pos.depth ? $pos.before() : undefined;
var node = parentPos !== undefined ? view.state.doc.nodeAt(parentPos) : undefined;
var nodeTypeWithLevel = $pos !== null && $pos !== void 0 && (_$pos$nodeAfter = $pos.nodeAfter) !== null && _$pos$nodeAfter !== void 0 && _$pos$nodeAfter.isBlock ? getNodeTypeWithLevel($pos.nodeAfter) : nodeType;
var parentNodeType = node === null || node === void 0 ? void 0 : node.type.name;
var supportsAnchor = CSS.supports('top', "anchor(".concat(anchorName, " start)")) && CSS.supports('left', "anchor(".concat(anchorName, " start)"));
var safeAnchorName = editorExperiment('platform_editor_controls', 'variant1') ? refreshAnchorName({
getPos: getPos,
view: view,
anchorName: anchorName
}) : anchorName;
var dom = view.dom.querySelector("[".concat(getAnchorAttrName(), "=\"").concat(safeAnchorName, "\"]"));
// Defence-in-depth guard: since the node decoration sets data-active-drag-handle on the
// active node, we check for it directly. This is a cheap DOM attribute read (no reflow,
// no style recalculation) and hides the control if the decoration hasn't been applied yet.
if (expValEquals('platform_editor_controls_reliable_anchor', 'isEnabled', true) && !(dom !== null && dom !== void 0 && dom.hasAttribute(ACTIVE_DRAG_HANDLE_ATTR))) {
return {
display: 'none'
};
}
var hasResizer = nodeType === 'table' || nodeType === 'mediaSingle';
var isExtension = nodeType === 'extension' || nodeType === 'bodiedExtension' || nodeType === 'multiBodiedExtension' && expValEquals('confluence_native_tabs_experiment', 'isEnabled', true);
var isBlockCard = nodeType === 'blockCard' && !!blockCardWidth;
var isEmbedCard = nodeType === 'embedCard';
var isMacroInteractionUpdates = macroInteractionUpdates && isExtension;
var innerContainer = null;
if (dom) {
if (isEmbedCard) {
innerContainer = dom.querySelector('.rich-media-item');
} else if (hasResizer) {
innerContainer = dom.querySelector('.resizer-item');
} else if (isExtension) {
innerContainer = dom.querySelector('.extension-container[data-layout]');
} else if (isBlockCard) {
//specific to datasource blockCard
innerContainer = dom.querySelector('.datasourceView-content-inner-wrap');
}
}
var isEdgeCase = (hasResizer || isExtension || isEmbedCard || isBlockCard) && innerContainer;
var isSticky = shouldBeSticky(nodeType);
if (supportsAnchor) {
var anchorStart = getDragHandleAnchorReference({
edge: 'start',
safeAnchorName: safeAnchorName
});
var anchorTop = getDragHandleAnchorReference({
edge: 'top',
safeAnchorName: safeAnchorName
});
var anchorLeft = getDragHandleAnchorReference({
edge: 'left',
safeAnchorName: safeAnchorName
});
var anchorRight = getDragHandleAnchorReference({
edge: 'right',
safeAnchorName: safeAnchorName
});
var bottom = editorExperiment('platform_editor_controls', 'variant1') ? getControlBottomCSSValue(safeAnchorName, isSticky, isTopLevelNodeValue, isLayoutColumn, ACTIVE_DRAG_HANDLE_FALLBACK_ANCHOR_NAME) : {};
return _objectSpread({
left: isEdgeCase ? "calc(".concat(anchorStart, " + ").concat(getLeftPosition(dom, nodeType, innerContainer, isMacroInteractionUpdates, parentNodeType), ")") : editorExperiment('advanced_layouts', true) && isLayoutColumn ? "calc((".concat(anchorRight, " + ").concat(anchorLeft, ")/2 - ").concat(DRAG_HANDLE_HEIGHT / 2, "px)") : "calc(".concat(anchorStart, " - ").concat(DRAG_HANDLE_WIDTH, "px - ").concat(dragHandleGap(nodeType, parentNodeType), "px)"),
top: editorExperiment('advanced_layouts', true) && isLayoutColumn ? "calc(".concat(anchorTop, " - ").concat(DRAG_HANDLE_WIDTH, "px)") : "calc(".concat(anchorStart, " + ").concat(topPositionAdjustment(nodeTypeWithLevel, (dom === null || dom === void 0 ? void 0 : dom.getAttribute('layout')) || ''), "px)")
}, bottom);
}
var height = editorExperiment('platform_editor_controls', 'variant1') ? getControlHeightCSSValue(getNodeHeight(dom, safeAnchorName, anchorRectCache) || 0, isSticky, isTopLevelNodeValue, "".concat(DRAG_HANDLE_HEIGHT), isLayoutColumn) : {};
return _objectSpread({
left: isEdgeCase ? "calc(".concat((dom === null || dom === void 0 ? void 0 : dom.offsetLeft) || 0, "px + ").concat(getLeftPosition(dom, nodeType, innerContainer, isMacroInteractionUpdates, parentNodeType), ")") : getLeftPosition(dom, nodeType, innerContainer, isMacroInteractionUpdates, parentNodeType),
top: getTopPosition(dom, nodeTypeWithLevel)
}, height);
}, [anchorName, getPos, view, nodeType, blockCardWidth, macroInteractionUpdates, anchorRectCache, isTopLevelNodeValue, isLayoutColumn]);
var isReliableAnchorEnabled = expValEquals('platform_editor_controls_reliable_anchor', 'isEnabled', true);
var docDepForReliableAnchor = isReliableAnchorEnabled ? view.state.doc : undefined;
// Effect 1 (reliable-anchor ON): fires when non-doc deps change (e.g. blockCardWidth,
// anchorRectCache, macroInteractionUpdates, isTopLevelNodeValue, isLayoutColumn via
// calculatePositionOld) — always recalculates without the doc guard, so non-doc position
// changes are never incorrectly skipped.
useEffect(function () {
if (!isReliableAnchorEnabled) {
return;
}
var cleanUpTransitionListener;
if (nodeType === 'extension' || nodeType === 'embedCard') {
var dom = view.dom.querySelector("[".concat(getAnchorAttrName(), "=\"").concat(anchorName, "\"]"));
if (!dom) {
return;
}
cleanUpTransitionListener = bind(dom, {
type: 'transitionend',
listener: function listener() {
setPositionStylesOld(calculatePositionOld());
}
});
}
var calcPos = requestAnimationFrame(function () {
setPositionStylesOld(calculatePositionOld());
hasCalculatedInitialPosition.current = true;
});
return function () {
var _cleanUpTransitionLis;
cancelAnimationFrame(calcPos);
(_cleanUpTransitionLis = cleanUpTransitionListener) === null || _cleanUpTransitionLis === void 0 || _cleanUpTransitionLis();
};
}, [isReliableAnchorEnabled, calculatePositionOld, view.dom, anchorName, nodeType]);
// Effect 2 (reliable-anchor ON): fires when the doc changes — carries the DOM-attribute
// guard to skip recalc when a drag handle is active (pure keystroke during drag).
// Effect (reliable-anchor OFF): single combined effect, original behaviour.
useEffect(function () {
if (isReliableAnchorEnabled) {
// Doc-change only effect: apply the DOM-attribute guard.
// React's dep comparison already ensures this only runs when docDepForReliableAnchor
// (i.e. view.state.doc) changed, so no manual ref tracking is needed.
if (!hasCalculatedInitialPosition.current) {
return;
}
var _calcPos = requestAnimationFrame(function () {
var dom = view.dom.querySelector("[".concat(getAnchorAttrName(), "=\"").concat(anchorName, "\"]"));
if (dom !== null && dom !== void 0 && dom.hasAttribute(ACTIVE_DRAG_HANDLE_ATTR)) {
return;
}
setPositionStylesOld(calculatePositionOld());
});
return function () {
cancelAnimationFrame(_calcPos);
};
}
// reliable-anchor OFF: original single-effect behaviour (no guard).
var cleanUpTransitionListener;
if (nodeType === 'extension' || nodeType === 'embedCard') {
var dom = view.dom.querySelector("[".concat(getAnchorAttrName(), "=\"").concat(anchorName, "\"]"));
if (!dom) {
return;
}
cleanUpTransitionListener = bind(dom, {
type: 'transitionend',
listener: function listener() {
setPositionStylesOld(calculatePositionOld());
}
});
}
var calcPos = requestAnimationFrame(function () {
setPositionStylesOld(calculatePositionOld());
hasCalculatedInitialPosition.current = true;
});
return function () {
var _cleanUpTransitionLis2;
cancelAnimationFrame(calcPos);
(_cleanUpTransitionLis2 = cleanUpTransitionListener) === null || _cleanUpTransitionLis2 === void 0 || _cleanUpTransitionLis2();
};
}, [isReliableAnchorEnabled, calculatePositionOld, view.dom, anchorName, nodeType, docDepForReliableAnchor]);
var isHandleShown = positionStylesOld.display !== 'none';
useEffect(function () {
if (isExperimentEnabled('platform_editor_react19_migration')) {
return;
}
if (handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused && buttonRef.current) {
var id = requestAnimationFrame(function () {
var _buttonRef$current;
(_buttonRef$current = buttonRef.current) === null || _buttonRef$current === void 0 || _buttonRef$current.focus();
});
return function () {
cancelAnimationFrame(id);
view.focus();
};
}
}, [buttonRef, handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view]);
useEffect(function () {
if (!isExperimentEnabled('platform_editor_react19_migration')) {
return;
}
if (!(handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused && isHandleShown && buttonRef.current)) {
return;
}
// isHandleShown means the handle has been positioned, but an ancestor can still be
// visibility:hidden or display:none for a frame, in which case focus() is a silent no-op.
// Focus now, and if activeElement shows it did not land, retry on the next few frames.
var rafId;
var attempts = 0;
var MAX_ATTEMPTS = 5;
var _focusHandle = function focusHandle() {
var _getDocument2;
var button = buttonRef.current;
if (!button) {
return;
}
button.focus();
// getDocument() is the same document handleKeyDownNew reads activeElement from, so
// this confirms Space/Enter will act on the handle rather than silently no-op.
if (((_getDocument2 = getDocument()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.activeElement) === button || attempts >= MAX_ATTEMPTS) {
return;
}
attempts++;
rafId = requestAnimationFrame(_focusHandle);
};
_focusHandle();
return function () {
if (rafId !== undefined) {
cancelAnimationFrame(rafId);
}
view.focus();
};
}, [buttonRef, handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view, isHandleShown]);
useEffect(function () {
if (typeof start !== 'number' || !selection) {
return;
}
setDragHandleSelected(isHandleCorrelatedToSelection(view.state, selection, start));
}, [start, selection, view]);
useEffect(function () {
var _api$blockControls$sh3;
if (isShiftDown === undefined || view.state.selection.empty || !fg('platform_editor_elements_dnd_shift_click_select')) {
return;
}
var mSelect = api === null || api === void 0 || (_api$blockControls$sh3 = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh3 === void 0 ? void 0 : _api$blockControls$sh3.multiSelectDnD;
var $anchor = (mSelect === null || mSelect === void 0 ? void 0 : mSelect.anchor) !== undefined ? view.state.doc.resolve(mSelect === null || mSelect === void 0 ? void 0 : mSelect.anchor) : view.state.selection.$anchor;
var isLayoutColumnMenuEnabled = expValEquals('platform_editor_layout_column_menu', 'isEnabled', true);
if (isShiftDown && !(isLayoutColumnMenuEnabled && isLayoutColumn) && (!isTopLevelNodeValue || isTopLevelNodeValue && $anchor.depth > DRAG_HANDLE_MAX_SHIFT_CLICK_DEPTH)) {
setDragHandleDisabled(true);
} else {
setDragHandleDisabled(false);
}
}, [api === null || api === void 0 || (_api$blockControls7 = api.blockControls) === null || _api$blockControls7 === void 0 ? void 0 : _api$blockControls7.sharedState, isLayoutColumn, isShiftDown, isTopLevelNodeValue, view]);
var dragHandleMessage = formatMessage(blockControlsMessages.dragToMoveClickToOpen, {
br: jsx("br", null)
});
// Create a string version for aria-label
var dragHandleAriaLabel = formatMessage(blockControlsMessages.dragToMoveClickToOpen, {
br: ' '
});
var helpDescriptors = isTopLevelNodeValue ? [{
description: dragHandleMessage
}, {
description: formatMessage(blockControlsMessages.moveUp),
keymap: dragToMoveUp
}, {
description: formatMessage(blockControlsMessages.moveDown),
keymap: dragToMoveDown
}, {
description: formatMessage(blockControlsMessages.moveLeft),
keymap: dragToMoveLeft
}, {
description: formatMessage(blockControlsMessages.moveRight),
keymap: dragToMoveRight
}] : [{
description: dragHandleMessage
}, {
description: formatMessage(blockControlsMessages.moveUp),
keymap: dragToMoveUp
}, {
description: formatMessage(blockControlsMessages.moveDown),
keymap: dragToMoveDown
}];
var isParentNodeOfTypeLayout;
if (!isTopLevelNodeValue) {
var pos = getPos();
if (typeof pos === 'number') {
var _$pos$parent;
var $pos = view.state.doc.resolve(pos);
isParentNodeOfTypeLayout = ($pos === null || $pos === void 0 || (_$pos$parent = $pos.parent) === null || _$pos$parent === void 0 ? void 0 : _$pos$parent.type.name) === 'layoutColumn';
}
if (isParentNodeOfTypeLayout) {
helpDescriptors = [].concat(_toConsumableArray(helpDescriptors), [{
description: formatMessage(blockControlsMessages.moveLeft),
keymap: dragToMoveLeft
}, {
description: formatMessage(blockControlsMessages.moveRight),
keymap: dragToMoveRight
}]);
}
}
// When advanced layout is on, layout column drag handle show only show 'Drag to move', no shortcuts
if (editorExperiment('advanced_layouts', true) && nodeType === 'layoutColumn') {
helpDescriptors = [{
description: formatMessage(blockControlsMessages.dragToRearrange)
}, {
description: formatMessage(blockControlsMessages.moveUp),
keymap: dragToMoveUp
}, {
description: formatMessage(blockControlsMessages.moveDown),
keymap: dragToMoveDown
}, {
description: formatMessage(blockControlsMessages.moveLeft),
keymap: dragToMoveLeft
}, {
description: formatMessage(blockControlsMessages.moveRight),
keymap: dragToMoveRight
}];
}
if (editorExperiment('platform_editor_controls', 'variant1')) {
helpDescriptors = [{
description: dragHandleMessage
}];
}
var message = helpDescriptors.map(function (descriptor) {
return descriptor.keymap ? [descriptor.description, getAriaKeyshortcuts(descriptor.keymap)] : [descriptor.description];
}).join('. ');
var handleOnDrop = function handleOnDrop(event) {
event.stopPropagation();
};
var hasHadInteraction = interactionState !== 'hasNotHadInteraction';
var browser = getBrowserInfo();
var renderButton = function renderButton() {
return (
// eslint-disable-next-line @atlaskit/design-system/no-html-button
jsx("button", {
type: "button",
css: [editorExperiment('platform_editor_controls', 'variant1') ? dragHandleButtonStyles : dragHandleButtonStylesOld, editorExperiment('platform_editor_controls', 'variant1') && dragHandleColor,
// ED-26266: Fixed the drag handle highlight when selecting multiple line in Firefox
// See https://product-fabric.atlassian.net/browse/ED-26266
browser.gecko && dragHandleMultiLineSelectionFixFirefox, editorExperiment('advanced_layouts', true) && isLayoutColumn && layoutColumnDragHandleStyles, dragHandleSelected && hasHadInteraction && selectedStyles, editorExperiment('platform_editor_preview_panel_responsiveness', true) && editorExperiment('platform_editor_controls', 'control') && dragHandleButtonSmallScreenStyles, isFocused && keyboardFocusedDragHandleStyles, focusedStyles, dragHandleButtonScaledStyles],
ref: buttonRef
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
,
style: !editorExperiment('platform_editor_controls', 'variant1') ? positionStylesOld : {},
onMouseDown: expValEqualsNoExposure('platform_editor_selection_toolbar_block_handle', 'isEnabled', true) ? handleMouseDown : undefined,
onMouseUp: handleMouseUp,
onClick: handleOnClickNew,
onKeyDown: handleKeyDownNew
// eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop
,
onDrop: handleOnDrop,
disabled: dragHandleDisabled,
"data-editor-block-ctrl-drag-handle": true,
"data-blocks-drag-handle": fg('confluence_remix_button_right_side_block_fg') || undefined,
"data-testid": "block-ctrl-drag-handle",
"aria-label": dragHandleAriaLabel,
onBlur: function onBlur() {
setIsFocused(false);
var pos = getPos();
if (pos !== undefined) {
var _api$core5;
api === null || api === void 0 || (_api$core5 = api.core) === null || _api$core5 === void 0 || _api$core5.actions.execute(function (_ref9) {
var tr = _ref9.tr;
tr.setMeta(key, {
activeNode: {
pos: pos,
anchorName: anchorName,
nodeType: nodeType,
handleOptions: {
isFocused: false
}
}
});
return tr;
});
}
}
}, jsx(Box, {
xcss: iconWrapperStyles
// eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop
,
onDragStart: handleIconDragStart
}, shouldUseNestedDragHandleIcon(isTopLevelNodeValue, isLayoutColumn) ? jsx(DragHandleNestedIcon, null) : jsx(DragHandleVerticalIcon, {
spacing: "spacious",
label: "",
size: "small"
})))
);
};
var stickyWithTooltip = function stickyWithTooltip() {
return jsx(Box
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
, {
style: positionStylesOld
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
xcss: [dragHandleContainerStyles],
as: "span",
testId: "block-ctrl-drag-handle-container"
}, jsx("span", {
css: [tooltipContainerStyles, shouldMaskNodeControls(nodeType, isTopLevelNodeValue) && (expValEquals('platform_editor_table_sticky_header_improvements', 'cohort', 'test_with_overflow') ? tooltipContainerStylesImprovedStickyHeaderWithMask : tooltipContainerStylesStickyHeaderWithMask), !shouldMaskNodeControls(nodeType, isTopLevelNodeValue) && tooltipContainerStylesStickyHeaderWithoutMask]
}, jsx(Tooltip, {
content: tooltipContent,
ignoreTooltipPointerEvents: true,
position: 'top',
tag: fg('platform-dst-top-layer-tooltip') ? 'span' : 'div'
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
onShow: function onShow() {
var _api$accessibilityUti;
api === null || api === void 0 || (_api$accessibilityUti = api.accessibilityUtils) === null || _api$accessibilityUti === void 0 || _api$accessibilityUti.actions.ariaNotify(message, {
priority: 'important'
});
}
}, jsx("span", {
css: [shouldMaskNodeControls(nodeType, isTopLevelNodeValue) && buttonWrapperStylesNoBackground, buttonWrapperStylesPatch]
}, renderButton()))));
};
var stickyWithoutTooltip = function stickyWithoutTooltip() {
return jsx(Box
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
, {
style: positionStylesOld
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
xcss: [dragHandleContainerStyles],
as: "span",
testId: "block-ctrl-drag-handle-container"
}, jsx("span", {
css: [tooltipContainerStyles, shouldMaskNodeControls(nodeType, isTopLevelNodeValue) && tooltipContainerStylesStickyHeaderWithMask, !shouldMaskNodeControls(nodeType, isTopLevelNodeValue) && tooltipContainerStylesStickyHeaderWithoutMask]
}, jsx("span", {
css: [shouldMaskNodeControls(nodeType, isTopLevelNodeValue) && buttonWrapperStylesNoBackground, buttonWrapperStylesPatch]
}, renderButton())));
};
var buttonWithTooltip = function buttonWithTooltip() {
return jsx(Tooltip, {
content: tooltipContent,
ignoreTooltipPointerEvents: true
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
onShow: function onShow() {
var _api$accessibilityUti2;
api === null || api === void 0 || (_api$accessibilityUti2 = api.accessibilityUtils) === null || _api$accessibilityUti2 === void 0 || _api$accessibilityUti2.actions.ariaNotify(message, {
priority: 'important'
});
}
}, renderButton());
};
var tooltipContent = isLayoutColumn && expValEquals('platform_editor_layout_column_menu', 'isEnabled', true) && currentUserIntent === 'layoutColumnMenuPopupOpen' ? null : jsx(TooltipContentWithMultipleShortcuts, {
helpDescriptors: helpDescriptors
});
var isTooltip = !dragHandleDisabled;
var stickyRender = isTooltip ? stickyWithTooltip() : stickyWithoutTooltip();
var render = isTooltip ? buttonWithTooltip() : renderButton();
return editorExperiment('platform_editor_controls', 'variant1') ? stickyRender : render;
};
export var DragHandleWithVisibility = function DragHandleWithVisibility(_ref0) {
var view = _ref0.view,
api = _ref0.api,
formatMessage = _ref0.formatMessage,
getPos = _ref0.getPos,
anchorName = _ref0.anchorName,
nodeType = _ref0.nodeType,
handleOptions = _ref0.handleOptions,
isTopLevelNode = _ref0.isTopLevelNode,
anchorRectCache = _ref0.anchorRectCache;
var rightSideControlsEnabled = useSharedPluginStateWithSelector(api, ['blockControls'], function (states) {
var _states$blockControls2, _states$blockControls3;
return {
rightSideControlsEnabled: (_states$blockControls2 = (_states$blockControls3 = states.blockControlsState) === null || _states$blockControls3 === void 0 ? void 0 : _states$blockControls3.rightSideControlsEnabled) !== null && _states$blockControls2 !== void 0 ? _states$blockControls2 : false
};
}).rightSideControlsEnabled;
// Layout column drag handles sit at the top-centre of each column, not on a left/right edge.
// Don't restrict by hoverSide for layout columns — the drag handle should always be visible
// when hovering anywhere over the column, regardless of which side of the layoutSection the
// column is on. (The right-side remix button is a separate node decoration and is unaffected.)
var isLayoutColumn = nodeType === 'layoutColumn';
// Skip the right-side controlSide restriction for non-top-level nodes. The restriction exists
// to avoid showing the drag handle and remix button simultaneously — but remix only applies to
// top-level nodes, so non-top-level nodes should never be restricted.
// Gated behind platform_editor_controls_reliable_anchor.
var skipControlSideRestriction = expValEquals('platform_editor_controls_reliable_anchor', 'isEnabled', true) && isTopLevelNode === false;
return jsx(VisibilityContainer, {
api: api,
controlSide: !isLayoutColumn && !skipControlSideRestriction && rightSideControlsEnabled ? 'left' : undefined,
forceVisibleOnMouseOut: !!(handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused),
shouldUseDisplayContents: isLayoutColumn && fg('platform-dst-top-layer-tooltip')
}, jsx(DragHandle, {
view: view,
api: api,
formatMessage: formatMessage,
getPos: getPos,
anchorName: anchorName,
nodeType: nodeType,
handleOptions: handleOptions,
anchorRectCache: anchorRectCache
}));
};