UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

1,193 lines 61.1 kB
/**
 * @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';
const iconWrapperStyles = xcss({
  display: 'flex',
  justifyContent: 'center',
  alignItems: 'center'
});
const 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(${"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(${"var(--ds-space-400, 32px)"} - 1px)`,
    marginRight: "var(--ds-space-negative-150, -12px)",
    paddingRight: "var(--ds-space-150, 12px)",
    boxSizing: 'border-box'
  }
});
const 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
const dragHandleColor = css({
  color: "var(--ds-icon-subtle, #505258)"
});
const 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
const 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)
});
const dragHandleButtonSmallScreenStyles = css({
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-container-queries, @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
  [`@container editor-area (max-width: ${akEditorFullPageNarrowBreakout}px)`]: {
    opacity: 0,
    visibility: 'hidden'
  }
});
const 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)"} solid ${"var(--ds-border-focused, #4688EC)"}`
  },
  '&:disabled': {
    color: "var(--ds-icon-disabled, #080F214A)",
    backgroundColor: 'transparent'
  },
  '&:hover:disabled': {
    backgroundColor: "var(--ds-background-disabled, #0515240F)"
  }
});
const focusedStyles = css({
  '&:focus-visible': {
    outline: `${"var(--ds-border-width-focused, 2px)"} solid ${"var(--ds-border-focused, #4688EC)"}`
  }
});
const keyboardFocusedDragHandleStyles = css({
  outline: `${"var(--ds-border-width-focused, 2px)"} solid ${"var(--ds-border-focused, #4688EC)"}`
});
const dragHandleContainerStyles = xcss({
  position: 'absolute',
  boxSizing: 'border-box'
});
const tooltipContainerStyles = css({
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
  bottom: `-${STICKY_CONTROLS_TOP_MARGIN}px`,
  position: 'sticky',
  display: 'block',
  zIndex: 100 // card = 100
});
const tooltipContainerStylesStickyHeaderWithMask = css({
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
  top: `${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'
  }
});
const tooltipContainerStylesImprovedStickyHeaderWithMask = css({
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
  top: `${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: `${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: `${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: `${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: `${STICKY_CONTROLS_TOP_MARGIN_FOR_STICKY_HEADER}px`
  }
});
const tooltipContainerStylesStickyHeaderWithoutMask = css({
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
  top: `${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
  }
});
const dragHandleMultiLineSelectionFixFirefox = css({
  '&::selection': {
    backgroundColor: 'transparent'
  }
});
const layoutColumnDragHandleStyles = css({
  transform: 'rotate(90deg)'
});
const 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
const handleIconDragStart = e => {
  const browser = getBrowserInfo();
  if (!browser.chrome) {
    return;
  }
  // prevent dragStart handler triggered by icon
  e.stopPropagation();
  const 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 ? void 0 : _e$target$closest.dispatchEvent(dragEvent);
  }
};
const getNodeSpacingForPreview = node => {
  if (!node) {
    return spacingBetweenNodesForPreview['default'];
  }
  const nodeTypeName = node.type.name;
  if (nodeTypeName === 'heading') {
    return spacingBetweenNodesForPreview[`heading${node.attrs.level}`] || spacingBetweenNodesForPreview['default'];
  }
  return spacingBetweenNodesForPreview[nodeTypeName] || spacingBetweenNodesForPreview['default'];
};
const getNodeMargins = node => {
  if (!node) {
    return nodeMargins['default'];
  }
  const nodeTypeName = node.type.name;
  if (nodeTypeName === 'heading') {
    return nodeMargins[`heading${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.
const buildToggleLayoutColumnMenuMeta = (anchorPos, openedViaKeyboard) => ({
  anchorPos,
  ...(fg('platform_editor_layout_column_menu_kill_switch_1') ? {
    isOpen: true
  } : {}),
  openedViaKeyboard
});
const getDragHandleAnchorReference = ({
  edge,
  safeAnchorName
}) => {
  if (expValEquals('platform_editor_controls_reliable_anchor', 'isEnabled', true)) {
    return `anchor(${safeAnchorName} ${edge}, anchor(${ACTIVE_DRAG_HANDLE_FALLBACK_ANCHOR_NAME} ${edge}))`;
  }
  return `anchor(${safeAnchorName} ${edge})`;
};
export const DragHandle = ({
  view,
  api,
  formatMessage,
  getPos,
  anchorName,
  nodeType,
  handleOptions,
  anchorRectCache
}) => {
  var _api$blockControls7;
  const buttonRef = useRef(null);
  const mouseDownRef = useRef(false);
  const [dragHandleSelected, setDragHandleSelected] = useState(false);
  const [dragHandleDisabled, setDragHandleDisabled] = useState(false);
  const [blockCardWidth, setBlockCardWidth] = useState(768);
  const [positionStylesOld, setPositionStylesOld] = useState({
    display: 'none'
  });
  // 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.
  const hasCalculatedInitialPosition = useRef(false);
  const [isFocused, setIsFocused] = useState(Boolean(handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused));
  const {
    macroInteractionUpdates,
    selection,
    isShiftDown,
    interactionState,
    currentUserIntent
  } = useSharedPluginStateWithSelector(api, ['featureFlags', 'selection', 'blockControls', 'interaction', 'userIntent'], 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
    };
  });
  const start = getPos();
  const isLayoutColumn = nodeType === 'layoutColumn';

  // Dynamically calculate if node is top-level based on current position
  const isTopLevelNodeValue = useMemo(() => {
    const pos = getPos();
    if (typeof pos === 'number') {
      const $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(() => {
    // blockCard/datasource width is rendered correctly after this decoraton does. We need to observe for changes.
    if (nodeType === 'blockCard') {
      const dom = view.dom.querySelector(`[${getAnchorAttrName()}="${anchorName}"]`);
      const container = dom === null || dom === void 0 ? void 0 : dom.querySelector('.datasourceView-content-inner-wrap');
      if (container) {
        const resizeObserver = new ResizeObserver(entries => {
          const width = entries[0].contentBoxSize[0].inlineSize;
          setBlockCardWidth(width);
        });
        resizeObserver.observe(container);
        return () => resizeObserver.unobserve(container);
      }
    }
  }, [anchorName, nodeType, view.dom]);
  useEffect(() => {
    if (!expValEqualsNoExposure('platform_editor_selection_toolbar_block_handle', 'isEnabled', true)) {
      return;
    }
    const unbind = bind(window, {
      type: 'mouseUp',
      listener: () => mouseDownRef.current = false
    });
    return () => unbind();
  }, []);
  const handleMouseDown = useCallback(() => {
    mouseDownRef.current = true;
  }, []);
  const handleMouseUp = useCallback(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();
    }
  }, []);
  const handleOnClickNew = useCallback(e => {
    var _api$core;
    api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
      tr
    }) => {
      var _selectionPreservatio, _api$analytics, _resolvedStartPos$nod, _api$blockControls, _api$blockControls2;
      const startPos = getPos();
      if (startPos === undefined) {
        return tr;
      }
      if (nodeType === 'layoutColumn' && expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
        tr.setMeta('toggleLayoutColumnMenu', buildToggleLayoutColumnMenuMeta(startPos, false));
      }
      const resolvedStartPos = tr.doc.resolve(startPos);
      const selection = ((_selectionPreservatio = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection) || tr.selection;
      api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? 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,
        selection,
        startPos,
        isShiftPressed: e.shiftKey,
        nodeType,
        api
      });
      api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.commands.startPreservingSelection()({
        tr
      });
      api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : _api$blockControls2.commands.toggleBlockMenu({
        anchorName,
        openedViaKeyboard: false,
        triggerByNode: {
          nodeType,
          pos: startPos,
          rootPos: tr.doc.resolve(startPos).before(1)
        }
      })({
        tr
      });
      tr.setMeta('scrollIntoView', false);
      return tr;
    });
    view.focus();
  }, [api, view, getPos, nodeType, anchorName]);
  const handleKeyDownNew = useCallback(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();
      const startPos = getPos();
      api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
        tr
      }) => {
        var _selectionPreservatio2, _api$blockControls3, _api$blockControls4, _api$userIntent;
        if (startPos === undefined) {
          return tr;
        }
        const selection = ((_selectionPreservatio2 = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio2 === void 0 ? void 0 : _selectionPreservatio2.preservedSelection) || tr.selection;
        expandAndUpdateSelection({
          tr,
          selection,
          startPos,
          isShiftPressed: e.shiftKey,
          nodeType,
          api
        });
        api === null || api === void 0 ? void 0 : (_api$blockControls3 = api.blockControls) === null || _api$blockControls3 === void 0 ? void 0 : _api$blockControls3.commands.startPreservingSelection()({
          tr
        });
        if (nodeType === 'layoutColumn' && expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
          tr.setMeta('toggleLayoutColumnMenu', buildToggleLayoutColumnMenuMeta(startPos, true));
        }
        const triggerByNode = {
          nodeType,
          pos: startPos,
          rootPos: tr.doc.resolve(startPos).before(1)
        };
        api === null || api === void 0 ? void 0 : (_api$blockControls4 = api.blockControls) === null || _api$blockControls4 === void 0 ? void 0 : _api$blockControls4.commands.toggleBlockMenu({
          anchorName,
          triggerByNode,
          openedViaKeyboard: true
        })({
          tr
        });
        api === null || api === void 0 ? void 0 : (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.commands.setCurrentUserIntent('blockMenuOpen')({
          tr
        });
        return tr;
      });
      view.focus();
    } else if (![e.altKey, e.ctrlKey, e.shiftKey].some(pressed => 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(() => {
    const element = buttonRef.current;
    if (!element) {
      return;
    }
    return draggable({
      element,
      getInitialData: () => ({
        type: 'element',
        start
      }),
      onGenerateDragPreview: ({
        nativeSetDragImage
      }) => {
        var _api$core3, _api$blockControls$sh;
        api === null || api === void 0 ? void 0 : (_api$core3 = api.core) === null || _api$core3 === void 0 ? void 0 : _api$core3.actions.execute(({
          tr
        }) => {
          const handlePos = getPos();
          if (typeof handlePos !== 'number') {
            return tr;
          }
          const newHandlePosCheck = isHandleCorrelatedToSelection(view.state, tr.selection, handlePos);
          if (!tr.selection.empty && newHandlePosCheck) {
            var _api$blockControls5;
            api === null || api === void 0 ? void 0 : (_api$blockControls5 = api.blockControls) === null || _api$blockControls5 === void 0 ? void 0 : _api$blockControls5.commands.setMultiSelectPositions()({
              tr
            });
          } else {
            tr = selectNode(tr, handlePos, nodeType, api);
          }
          return tr;
        });
        const startPos = getPos();
        const state = view.state;
        const {
          doc,
          selection
        } = state;
        let sliceFrom = selection.from;
        let sliceTo = selection.to;
        const mSelect = api === null || api === void 0 ? void 0 : (_api$blockControls$sh = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh === void 0 ? void 0 : _api$blockControls$sh.multiSelectDnD;
        if (mSelect) {
          const {
            anchor,
            head
          } = mSelect;
          sliceFrom = Math.min(anchor, head);
          sliceTo = Math.max(anchor, head);
        }
        const expandedSlice = doc.slice(sliceFrom, sliceTo);
        const isDraggingMultiLine = startPos !== undefined && startPos >= sliceFrom && startPos < sliceTo && expandedSlice.content.childCount > 1;
        setCustomNativeDragPreview({
          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
              const domAtPos = view.domAtPos.bind(view);
              let domElementsHeightBeforeHandle = 0;
              const nodesStartPos = [];
              const nodesEndPos = [];
              let activeNodeMarginTop = 0;
              for (let 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
                  const currentNodeElement = findDomRefAtPos(nodesStartPos[i], domAtPos);
                  const maybeCurrentNode = expandedSlice.content.maybeChild(i);
                  const 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
                  const maybeNextNode = expandedSlice.content.maybeChild(i);
                  activeNodeMarginTop = maybeNextNode ? getNodeMargins(maybeNextNode).top : 0;
                  break;
                }
              }
              return {
                x: 0,
                y: domElementsHeightBeforeHandle + activeNodeMarginTop
              };
            }
          },
          render: ({
            container
          }) => {
            const dom = view.dom.querySelector(`[${getAnchorAttrName()}="${anchorName}"]`);
            if (!dom) {
              return;
            }
            if (!isDraggingMultiLine) {
              return dragPreview(container, {
                dom,
                nodeType
              });
            } else {
              const domAtPos = view.domAtPos.bind(view);
              const previewContent = [];
              expandedSlice.content.descendants((node, pos) => {
                // Get the dom element of the node
                //eslint-disable-next-line @atlaskit/editor/no-as-casting
                const nodeDomElement = findDomRefAtPos(sliceFrom + pos, domAtPos);
                const 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
        });
      },
      onDragStart() {
        var _api$core4;
        if (start === undefined) {
          return;
        }
        api === null || api === void 0 ? void 0 : (_api$core4 = api.core) === null || _api$core4 === void 0 ? void 0 : _api$core4.actions.execute(({
          tr
        }) => {
          var _api$blockControls$sh2, _api$blockControls6, _api$analytics2;
          let nodeTypes, hasSelectedMultipleNodes;
          const resolvedMovingNode = tr.doc.resolve(start);
          const maybeNode = resolvedMovingNode.nodeAfter;
          const mSelect = api === null || api === void 0 ? void 0 : (_api$blockControls$sh2 = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh2 === void 0 ? void 0 : _api$blockControls$sh2.multiSelectDnD;
          if (mSelect) {
            const 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 ? void 0 : (_api$blockControls6 = api.blockControls) === null || _api$blockControls6 === void 0 ? void 0 : _api$blockControls6.commands.setNodeDragged(getPos, anchorName, nodeType)({
            tr
          });
          tr.setMeta('scrollIntoView', false);
          api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? 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
            }
          })(tr);
          return tr;
        });
        view.focus();
      }
    });
  }, [anchorName, api, getPos, nodeType, start, view]);
  const calculatePositionOld = useCallback(() => {
    var _$pos$nodeAfter;
    const pos = getPos();
    const $pos = typeof pos === 'number' ? view.state.doc.resolve(pos) : undefined;
    const parentPos = $pos && $pos.depth ? $pos.before() : undefined;
    const node = parentPos !== undefined ? view.state.doc.nodeAt(parentPos) : undefined;
    const nodeTypeWithLevel = $pos !== null && $pos !== void 0 && (_$pos$nodeAfter = $pos.nodeAfter) !== null && _$pos$nodeAfter !== void 0 && _$pos$nodeAfter.isBlock ? getNodeTypeWithLevel($pos.nodeAfter) : nodeType;
    const parentNodeType = node === null || node === void 0 ? void 0 : node.type.name;
    const supportsAnchor = CSS.supports('top', `anchor(${anchorName} start)`) && CSS.supports('left', `anchor(${anchorName} start)`);
    const safeAnchorName = editorExperiment('platform_editor_controls', 'variant1') ? refreshAnchorName({
      getPos,
      view,
      anchorName
    }) : anchorName;
    const dom = view.dom.querySelector(`[${getAnchorAttrName()}="${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'
      };
    }
    const hasResizer = nodeType === 'table' || nodeType === 'mediaSingle';
    const isExtension = nodeType === 'extension' || nodeType === 'bodiedExtension' || nodeType === 'multiBodiedExtension' && expValEquals('confluence_native_tabs_experiment', 'isEnabled', true);
    const isBlockCard = nodeType === 'blockCard' && !!blockCardWidth;
    const isEmbedCard = nodeType === 'embedCard';
    const isMacroInteractionUpdates = macroInteractionUpdates && isExtension;
    let 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');
      }
    }
    const isEdgeCase = (hasResizer || isExtension || isEmbedCard || isBlockCard) && innerContainer;
    const isSticky = shouldBeSticky(nodeType);
    if (supportsAnchor) {
      const anchorStart = getDragHandleAnchorReference({
        edge: 'start',
        safeAnchorName
      });
      const anchorTop = getDragHandleAnchorReference({
        edge: 'top',
        safeAnchorName
      });
      const anchorLeft = getDragHandleAnchorReference({
        edge: 'left',
        safeAnchorName
      });
      const anchorRight = getDragHandleAnchorReference({
        edge: 'right',
        safeAnchorName
      });
      const bottom = editorExperiment('platform_editor_controls', 'variant1') ? getControlBottomCSSValue(safeAnchorName, isSticky, isTopLevelNodeValue, isLayoutColumn, ACTIVE_DRAG_HANDLE_FALLBACK_ANCHOR_NAME) : {};
      return {
        left: isEdgeCase ? `calc(${anchorStart} + ${getLeftPosition(dom, nodeType, innerContainer, isMacroInteractionUpdates, parentNodeType)})` : editorExperiment('advanced_layouts', true) && isLayoutColumn ? `calc((${anchorRight} + ${anchorLeft})/2 - ${DRAG_HANDLE_HEIGHT / 2}px)` : `calc(${anchorStart} - ${DRAG_HANDLE_WIDTH}px - ${dragHandleGap(nodeType, parentNodeType)}px)`,
        top: editorExperiment('advanced_layouts', true) && isLayoutColumn ? `calc(${anchorTop} - ${DRAG_HANDLE_WIDTH}px)` : `calc(${anchorStart} + ${topPositionAdjustment(nodeTypeWithLevel, (dom === null || dom === void 0 ? void 0 : dom.getAttribute('layout')) || '')}px)`,
        ...bottom
      };
    }
    const height = editorExperiment('platform_editor_controls', 'variant1') ? getControlHeightCSSValue(getNodeHeight(dom, safeAnchorName, anchorRectCache) || 0, isSticky, isTopLevelNodeValue, `${DRAG_HANDLE_HEIGHT}`, isLayoutColumn) : {};
    return {
      left: isEdgeCase ? `calc(${(dom === null || dom === void 0 ? void 0 : dom.offsetLeft) || 0}px + ${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]);
  const isReliableAnchorEnabled = expValEquals('platform_editor_controls_reliable_anchor', 'isEnabled', true);
  const 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(() => {
    if (!isReliableAnchorEnabled) {
      return;
    }
    let cleanUpTransitionListener;
    if (nodeType === 'extension' || nodeType === 'embedCard') {
      const dom = view.dom.querySelector(`[${getAnchorAttrName()}="${anchorName}"]`);
      if (!dom) {
        return;
      }
      cleanUpTransitionListener = bind(dom, {
        type: 'transitionend',
        listener: () => {
          setPositionStylesOld(calculatePositionOld());
        }
      });
    }
    const calcPos = requestAnimationFrame(() => {
      setPositionStylesOld(calculatePositionOld());
      hasCalculatedInitialPosition.current = true;
    });
    return () => {
      var _cleanUpTransitionLis;
      cancelAnimationFrame(calcPos);
      (_cleanUpTransitionLis = cleanUpTransitionListener) === null || _cleanUpTransitionLis === void 0 ? 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(() => {
    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;
      }
      const calcPos = requestAnimationFrame(() => {
        const dom = view.dom.querySelector(`[${getAnchorAttrName()}="${anchorName}"]`);
        if (dom !== null && dom !== void 0 && dom.hasAttribute(ACTIVE_DRAG_HANDLE_ATTR)) {
          return;
        }
        setPositionStylesOld(calculatePositionOld());
      });
      return () => {
        cancelAnimationFrame(calcPos);
      };
    }

    // reliable-anchor OFF: original single-effect behaviour (no guard).
    let cleanUpTransitionListener;
    if (nodeType === 'extension' || nodeType === 'embedCard') {
      const dom = view.dom.querySelector(`[${getAnchorAttrName()}="${anchorName}"]`);
      if (!dom) {
        return;
      }
      cleanUpTransitionListener = bind(dom, {
        type: 'transitionend',
        listener: () => {
          setPositionStylesOld(calculatePositionOld());
        }
      });
    }
    const calcPos = requestAnimationFrame(() => {
      setPositionStylesOld(calculatePositionOld());
      hasCalculatedInitialPosition.current = true;
    });
    return () => {
      var _cleanUpTransitionLis2;
      cancelAnimationFrame(calcPos);
      (_cleanUpTransitionLis2 = cleanUpTransitionListener) === null || _cleanUpTransitionLis2 === void 0 ? void 0 : _cleanUpTransitionLis2();
    };
  }, [isReliableAnchorEnabled, calculatePositionOld, view.dom, anchorName, nodeType, docDepForReliableAnchor]);
  const isHandleShown = positionStylesOld.display !== 'none';
  useEffect(() => {
    if (isExperimentEnabled('platform_editor_react19_migration')) {
      return;
    }
    if (handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused && buttonRef.current) {
      const id = requestAnimationFrame(() => {
        var _buttonRef$current;
        (_buttonRef$current = buttonRef.current) === null || _buttonRef$current === void 0 ? void 0 : _buttonRef$current.focus();
      });
      return () => {
        cancelAnimationFrame(id);
        view.focus();
      };
    }
  }, [buttonRef, handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view]);
  useEffect(() => {
    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.
    let rafId;
    let attempts = 0;
    const MAX_ATTEMPTS = 5;
    const focusHandle = () => {
      var _getDocument2;
      const 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 () => {
      if (rafId !== undefined) {
        cancelAnimationFrame(rafId);
      }
      view.focus();
    };
  }, [buttonRef, handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view, isHandleShown]);
  useEffect(() => {
    if (typeof start !== 'number' || !selection) {
      return;
    }
    setDragHandleSelected(isHandleCorrelatedToSelection(view.state, selection, start));
  }, [start, selection, view]);
  useEffect(() => {
    var _api$blockControls$sh3;
    if (isShiftDown === undefined || view.state.selection.empty || !fg('platform_editor_elements_dnd_shift_click_select')) {
      return;
    }
    const mSelect = api === null || api === void 0 ? void 0 : (_api$blockControls$sh3 = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh3 === void 0 ? void 0 : _api$blockControls$sh3.multiSelectDnD;
    const $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;
    const 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 ? void 0 : (_api$blockControls7 = api.blockControls) === null || _api$blockControls7 === void 0 ? void 0 : _api$blockControls7.sharedState, isLayoutColumn, isShiftDown, isTopLevelNodeValue, view]);
  const dragHandleMessage = formatMessage(blockControlsMessages.dragToMoveClickToOpen, {
    br: jsx("br", null)
  });

  // Create a string version for aria-label
  const dragHandleAriaLabel = formatMessage(blockControlsMessages.dragToMoveClickToOpen, {
    br: ' '
  });
  let 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
  }];
  let isParentNodeOfTypeLayout;
  if (!isTopLevelNodeValue) {
    const pos = getPos();
    if (typeof pos === 'number') {
      var _$pos$parent;
      const $pos = view.state.doc.resolve(pos);
      isParentNodeOfTypeLayout = ($pos === null || $pos === void 0 ? void 0 : (_$pos$parent = $pos.parent) === null || _$pos$parent === void 0 ? void 0 : _$pos$parent.type.name) === 'layoutColumn';
    }
    if (isParentNodeOfTypeLayout) {
      helpDescriptors = [...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
    }];
  }
  const message = helpDescriptors.map(descriptor => {
    return descriptor.keymap ? [descriptor.description, getAriaKeyshortcuts(descriptor.keymap)] : [descriptor.description];
  }).join('. ');
  const handleOnDrop = event => {
    event.stopPropagation();
  };
  const hasHadInteraction = interactionState !== 'hasNotHadInteraction';
  const browser = getBrowserInfo();
  const renderButton = () =>
  // 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: () => {
      setIsFocused(false);
      const pos = getPos();
      if (pos !== undefined) {
        var _api$core5;
        api === null || api === void 0 ? void 0 : (_api$core5 = api.core) === null || _api$core5 === void 0 ? void 0 : _api$core5.actions.execute(({
          tr
        }) => {
          tr.setMeta(key, {
            activeNode: {
              pos,
              anchorName,
              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"
  })));
  const stickyWithTooltip = () => 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: () => {
      var _api$accessibilityUti;
      api === null || api === void 0 ? void 0 : (_api$accessibilityUti = api.accessibilityUtils) === null || _api$accessibilityUti === void 0 ? void 0 : _api$accessibilityUti.actions.ariaNotify(message, {
        priority: 'important'
      });
    }
  }, jsx("span", {
    css: [shouldMaskNodeControls(nodeType, isTopLevelNodeValue) && buttonWrapperStylesNoBackground, buttonWrapperStylesPatch]
  }, renderButton()))));
  const stickyWithoutTooltip = () => 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())));
  const buttonWithTooltip = () => 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: () => {
      var _api$accessibilityUti2;
      api === null || api === void 0 ? void 0 : (_api$accessibilityUti2 = api.accessibilityUtils) === null || _api$accessibilityUti2 === void 0 ? void 0 : _api$accessibilityUti2.actions.ariaNotify(message, {
        priority: 'important'
      });
    }
  }, renderButton());
  const tooltipContent = isLayoutColumn && expValEquals('platform_editor_layout_column_menu', 'isEnabled', true) && currentUserIntent === 'layoutColumnMenuPopupOpen' ? null : jsx(TooltipContentWithMultipleShortcuts, {
    helpDescriptors: helpDescriptors
  });
  const isTooltip = !dragHandleDisabled;
  const stickyRender = isTooltip ? stickyWithTooltip() : stickyWithoutTooltip();
  const render = isTooltip ? buttonWithTooltip() : renderButton();
  return editorExperiment('platform_editor_controls', 'variant1') ? stickyRender : render;
};
export const DragHandleWithVisibility = ({
  view,
  api,
  formatMessage,
  getPos,
  anchorName,
  nodeType,
  handleOptions,
  isTopLevelNode,
  anchorRectCache
}) => {
  const rightSideControlsEnabled = useSharedPluginStateWithSelector(api, ['blockControls'], 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.)
  const 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.
  const 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
  }));
};