@atlaskit/editor-plugin-block-controls 17.1.1 → 17.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/blockControlsPlugin.js +14 -6
  3. package/dist/cjs/pm-plugins/main.js +14 -6
  4. package/dist/cjs/ui/block-controls-surface-context.js +2 -1
  5. package/dist/cjs/ui/block-controls-surface-drag-handle-registration.js +37 -0
  6. package/dist/cjs/ui/block-controls-surface-drag-handle-utils.js +45 -0
  7. package/dist/cjs/ui/block-controls-surface-drag-handle.compiled.css +32 -0
  8. package/dist/cjs/ui/block-controls-surface-drag-handle.js +296 -0
  9. package/dist/cjs/ui/block-controls-surface.compiled.css +2 -1
  10. package/dist/cjs/ui/block-controls-surface.js +49 -24
  11. package/dist/cjs/ui/consts.js +2 -1
  12. package/dist/cjs/ui/surface-editor-view-context.js +12 -0
  13. package/dist/cjs/ui/use-block-controls-surface-drag-source.js +172 -0
  14. package/dist/cjs/ui/utils/get-surface-placement.js +8 -1
  15. package/dist/es2019/blockControlsPlugin.js +13 -6
  16. package/dist/es2019/pm-plugins/main.js +14 -7
  17. package/dist/es2019/ui/block-controls-surface-context.js +2 -2
  18. package/dist/es2019/ui/block-controls-surface-drag-handle-registration.js +26 -0
  19. package/dist/es2019/ui/block-controls-surface-drag-handle-utils.js +33 -0
  20. package/dist/es2019/ui/block-controls-surface-drag-handle.compiled.css +32 -0
  21. package/dist/es2019/ui/block-controls-surface-drag-handle.js +282 -0
  22. package/dist/es2019/ui/block-controls-surface.compiled.css +2 -1
  23. package/dist/es2019/ui/block-controls-surface.js +48 -22
  24. package/dist/es2019/ui/consts.js +1 -0
  25. package/dist/es2019/ui/surface-editor-view-context.js +4 -0
  26. package/dist/es2019/ui/use-block-controls-surface-drag-source.js +170 -0
  27. package/dist/es2019/ui/utils/get-surface-placement.js +15 -6
  28. package/dist/esm/blockControlsPlugin.js +14 -6
  29. package/dist/esm/pm-plugins/main.js +14 -6
  30. package/dist/esm/ui/block-controls-surface-context.js +2 -1
  31. package/dist/esm/ui/block-controls-surface-drag-handle-registration.js +30 -0
  32. package/dist/esm/ui/block-controls-surface-drag-handle-utils.js +38 -0
  33. package/dist/esm/ui/block-controls-surface-drag-handle.compiled.css +32 -0
  34. package/dist/esm/ui/block-controls-surface-drag-handle.js +290 -0
  35. package/dist/esm/ui/block-controls-surface.compiled.css +2 -1
  36. package/dist/esm/ui/block-controls-surface.js +49 -24
  37. package/dist/esm/ui/consts.js +1 -0
  38. package/dist/esm/ui/surface-editor-view-context.js +6 -0
  39. package/dist/esm/ui/use-block-controls-surface-drag-source.js +166 -0
  40. package/dist/esm/ui/utils/get-surface-placement.js +9 -2
  41. package/dist/types/pm-plugins/main.d.ts +6 -1
  42. package/dist/types/ui/block-controls-surface-context.d.ts +1 -1
  43. package/dist/types/ui/block-controls-surface-drag-handle-registration.d.ts +6 -0
  44. package/dist/types/ui/block-controls-surface-drag-handle-utils.d.ts +14 -0
  45. package/dist/types/ui/block-controls-surface-drag-handle.d.ts +10 -0
  46. package/dist/types/ui/block-controls-surface.d.ts +1 -0
  47. package/dist/types/ui/consts.d.ts +1 -0
  48. package/dist/types/ui/surface-editor-view-context.d.ts +5 -0
  49. package/dist/types/ui/use-block-controls-surface-drag-source.d.ts +19 -0
  50. package/dist/types/ui/utils/get-surface-placement.d.ts +1 -1
  51. package/package.json +3 -2
@@ -0,0 +1,282 @@
1
+ /* block-controls-surface-drag-handle.tsx generated by @compiled/babel-plugin v2.0.0 */
2
+ import "./block-controls-surface-drag-handle.compiled.css";
3
+ import * as React from 'react';
4
+ import { ax, ix } from "@compiled/react/runtime";
5
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
6
+ import { useIntl } from 'react-intl';
7
+
8
+ // oxlint-disable-next-line typescript/consistent-type-imports -- jsx is the runtime factory for the classic JSX pragma above.
9
+
10
+ import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
11
+ import { BLOCK_CONTROL_UI_CONTEXT } from '@atlaskit/editor-common/block-controls/block-control-ui-context';
12
+ import { surfaceDragHandleElementStore } from '@atlaskit/editor-common/block-controls/surface-drag-handle-element';
13
+ import { getBrowserInfo } from '@atlaskit/editor-common/browser';
14
+ import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
15
+ import { TooltipContentWithMultipleShortcuts } from '@atlaskit/editor-common/keymaps';
16
+ import { blockControlsMessages } from '@atlaskit/editor-common/messages';
17
+ import DragHandleVerticalIcon from '@atlaskit/icon/core/drag-handle-vertical';
18
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
19
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
20
+ import Tooltip from '@atlaskit/tooltip/Tooltip';
21
+ import { selectionPreservationPluginKey } from '../pm-plugins/selection-preservation/plugin-key';
22
+ import { expandAndUpdateSelection } from '../pm-plugins/utils/expand-and-update-selection';
23
+ import { isHandleCorrelatedToSelection } from '../pm-plugins/utils/getSelection';
24
+ import { buildLayoutColumnMenuMeta } from './block-controls-surface-drag-handle-utils';
25
+ import { DRAG_HANDLE_MAX_SHIFT_CLICK_DEPTH } from './consts';
26
+ import { DragHandleNestedIcon } from './drag-handle-nested-icon';
27
+ import { shouldUseNestedDragHandleIcon } from './should-use-nested-drag-handle-icon';
28
+ import { useSurfaceEditorView } from './surface-editor-view-context';
29
+ import { useBlockControlsSurfaceDragSource } from './use-block-controls-surface-drag-source';
30
+ import { getAnchorAttrName, NODE_ANCHOR_ATTR_NAME } from './utils/dom-attr-name';
31
+
32
+ // Keep style values co-located for Compiled while matching the legacy drag handle dimensions.
33
+ const SURFACE_DRAG_HANDLE_HEIGHT = 24;
34
+ const SURFACE_DRAG_HANDLE_WIDTH = 12;
35
+ const SURFACE_DRAG_HANDLE_BORDER_RADIUS = 4;
36
+ const SURFACE_DRAG_HANDLE_Z_INDEX = 100;
37
+ const dragHandleStyles = {
38
+ root: "_19itglyw _2rko1y44 _12ji1r31 _1qu2glyw _12y31o36 _1e0c1txw _vchhusvi _2lx21bp4 _4cvr1h6o _1bah1h6o _4t3i1tcg _1bsb1crf _bfhksm61 _syazdgkc _80om1qgj _1pbyb4wl _ca0qze3t _u5f3ze3t _n3tdze3t _19bvze3t _cun91j28 _vbqb2sac _1ygbdfik _irr31dpa _6g80187o _1di6fcek",
39
+ scaled: "_4t3i1myc _1bsbx5ub",
40
+ selected: "_bfhk15s3 _syazxl6c",
41
+ keyboardFocused: "_1a37dfik",
42
+ firefoxSelection: "_1p1y1j28",
43
+ layoutColumn: "_t9ec14w8",
44
+ iconWrapper: "_1e0c1txw _4cvr1h6o _1bah1h6o"
45
+ };
46
+ const handleIconDragStart = event => {
47
+ var _event$currentTarget$;
48
+ if (!getBrowserInfo().chrome) {
49
+ return;
50
+ }
51
+ event.stopPropagation();
52
+ (_event$currentTarget$ = event.currentTarget.parentElement) === null || _event$currentTarget$ === void 0 ? void 0 : _event$currentTarget$.dispatchEvent(new DragEvent('dragstart', {
53
+ bubbles: true,
54
+ cancelable: true,
55
+ dataTransfer: event.dataTransfer
56
+ }));
57
+ };
58
+ const isSelected = (selection, pos, view) => {
59
+ if (typeof pos !== 'number' || !selection || !view) {
60
+ return false;
61
+ }
62
+ return isHandleCorrelatedToSelection(view.state, selection, pos);
63
+ };
64
+ export const BlockControlsSurfaceDragHandle = ({
65
+ api,
66
+ surfaceContext
67
+ }) => {
68
+ var _activeNode$type$name;
69
+ const {
70
+ formatMessage
71
+ } = useIntl();
72
+ const view = useSurfaceEditorView();
73
+ const context = surfaceContext === null || surfaceContext === void 0 ? void 0 : surfaceContext.get(BLOCK_CONTROL_UI_CONTEXT);
74
+ const activeNode = context === null || context === void 0 ? void 0 : context.activeNode;
75
+ const pos = activeNode === null || activeNode === void 0 ? void 0 : activeNode.pos;
76
+ const nodeType = (_activeNode$type$name = activeNode === null || activeNode === void 0 ? void 0 : activeNode.type.name) !== null && _activeNode$type$name !== void 0 ? _activeNode$type$name : '';
77
+ const buttonRef = useRef(null);
78
+ const posRef = useRef(pos);
79
+ posRef.current = pos;
80
+ const getPos = useCallback(() => posRef.current, []);
81
+ const {
82
+ activeNodeAnchorName,
83
+ handleOptions,
84
+ interactionState,
85
+ isShiftDown,
86
+ selection
87
+ } = useSharedPluginStateWithSelector(api, ['blockControls', 'interaction', 'selection'], states => {
88
+ var _states$blockControls, _states$blockControls2, _states$blockControls3, _states$blockControls4, _states$interactionSt, _states$blockControls5, _states$selectionStat;
89
+ return {
90
+ activeNodeAnchorName: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : (_states$blockControls2 = _states$blockControls.activeNode) === null || _states$blockControls2 === void 0 ? void 0 : _states$blockControls2.anchorName,
91
+ handleOptions: (_states$blockControls3 = states.blockControlsState) === null || _states$blockControls3 === void 0 ? void 0 : (_states$blockControls4 = _states$blockControls3.activeNode) === null || _states$blockControls4 === void 0 ? void 0 : _states$blockControls4.handleOptions,
92
+ interactionState: (_states$interactionSt = states.interactionState) === null || _states$interactionSt === void 0 ? void 0 : _states$interactionSt.interactionState,
93
+ isShiftDown: (_states$blockControls5 = states.blockControlsState) === null || _states$blockControls5 === void 0 ? void 0 : _states$blockControls5.isShiftDown,
94
+ selection: (_states$selectionStat = states.selectionState) === null || _states$selectionStat === void 0 ? void 0 : _states$selectionStat.selection
95
+ };
96
+ });
97
+ const [dragHandleSelected, setDragHandleSelected] = useState(false);
98
+ useEffect(() => {
99
+ setDragHandleSelected(isSelected(selection, pos, view));
100
+ }, [pos, selection, view]);
101
+ const isLayoutColumn = nodeType === 'layoutColumn';
102
+ const isTopLevelNode = (activeNode === null || activeNode === void 0 ? void 0 : activeNode.parentType) === 'doc';
103
+ const isDisabled = useMemo(() => {
104
+ var _api$blockControls$sh;
105
+ if (!isShiftDown || !view || view.state.selection.empty || !fg('platform_editor_elements_dnd_shift_click_select')) {
106
+ return false;
107
+ }
108
+ if (isLayoutColumn && expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
109
+ return false;
110
+ }
111
+ const multiSelect = 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;
112
+ const $anchor = (multiSelect === null || multiSelect === void 0 ? void 0 : multiSelect.anchor) !== undefined ? view.state.doc.resolve(multiSelect.anchor) : view.state.selection.$anchor;
113
+ return !isTopLevelNode || $anchor.depth > DRAG_HANDLE_MAX_SHIFT_CLICK_DEPTH;
114
+ }, [api, isLayoutColumn, isShiftDown, isTopLevelNode, view]);
115
+ const getAnchorName = useCallback(() => {
116
+ var _ref, _ref2, _dom$getAttribute, _dom$closest;
117
+ if (activeNodeAnchorName) {
118
+ return activeNodeAnchorName;
119
+ }
120
+ const currentPos = posRef.current;
121
+ if (!view || currentPos === undefined) {
122
+ return '';
123
+ }
124
+ const dom = view.nodeDOM(currentPos);
125
+ if (!(dom instanceof HTMLElement)) {
126
+ return '';
127
+ }
128
+ const anchorAttrName = getAnchorAttrName();
129
+ return (_ref = (_ref2 = (_dom$getAttribute = dom.getAttribute(anchorAttrName)) !== null && _dom$getAttribute !== void 0 ? _dom$getAttribute : dom.getAttribute(NODE_ANCHOR_ATTR_NAME)) !== null && _ref2 !== void 0 ? _ref2 : (_dom$closest = dom.closest(`[${anchorAttrName}]`)) === null || _dom$closest === void 0 ? void 0 : _dom$closest.getAttribute(anchorAttrName)) !== null && _ref !== void 0 ? _ref : '';
130
+ }, [activeNodeAnchorName, view]);
131
+ useLayoutEffect(() => {
132
+ const element = buttonRef.current;
133
+ if (!view || !element) {
134
+ return;
135
+ }
136
+ surfaceDragHandleElementStore.set(view, element);
137
+ return () => surfaceDragHandleElementStore.release(view, element);
138
+ }, [view]);
139
+ useEffect(() => {
140
+ if (!(handleOptions !== null && handleOptions !== void 0 && handleOptions.isFocused) || !buttonRef.current || !view) {
141
+ return;
142
+ }
143
+ let animationFrameId;
144
+ let attempts = 0;
145
+ const focusHandle = () => {
146
+ const button = buttonRef.current;
147
+ if (!button) {
148
+ return;
149
+ }
150
+ button.focus();
151
+ if (button.ownerDocument.activeElement !== button && attempts < 5) {
152
+ attempts++;
153
+ animationFrameId = requestAnimationFrame(focusHandle);
154
+ }
155
+ };
156
+ focusHandle();
157
+ return () => {
158
+ if (animationFrameId !== undefined) {
159
+ cancelAnimationFrame(animationFrameId);
160
+ }
161
+ view.focus();
162
+ };
163
+ }, [handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused, view]);
164
+ useBlockControlsSurfaceDragSource({
165
+ api,
166
+ elementRef: buttonRef,
167
+ getAnchorName,
168
+ getPos,
169
+ nodeType,
170
+ start: pos,
171
+ view
172
+ });
173
+ const handleMouseDown = useCallback(() => {
174
+ var _api$core, _api$blockControls;
175
+ api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.commands.setSelectedViaDragHandle(true));
176
+ }, [api]);
177
+ const handleClick = useCallback(event => {
178
+ var _api$core2;
179
+ if (!view || isDisabled) {
180
+ return;
181
+ }
182
+ const openedViaKeyboard = event.detail === 0;
183
+ api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
184
+ tr
185
+ }) => {
186
+ var _selectionPreservatio, _selectionPreservatio2, _api$analytics, _resolvedStartPos$nod, _api$blockControls2, _api$blockControls3, _activeNode$rootPos;
187
+ const startPos = getPos();
188
+ if (startPos === undefined) {
189
+ return tr;
190
+ }
191
+ if (nodeType === 'layoutColumn' && expValEquals('platform_editor_layout_column_menu', 'isEnabled', true)) {
192
+ tr.setMeta('toggleLayoutColumnMenu', buildLayoutColumnMenuMeta(startPos, openedViaKeyboard));
193
+ }
194
+ const selection = (_selectionPreservatio = (_selectionPreservatio2 = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio2 === void 0 ? void 0 : _selectionPreservatio2.preservedSelection) !== null && _selectionPreservatio !== void 0 ? _selectionPreservatio : tr.selection;
195
+ const resolvedStartPos = tr.doc.resolve(startPos);
196
+ api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.attachAnalyticsEvent({
197
+ action: ACTION.CLICKED,
198
+ actionSubject: ACTION_SUBJECT.BUTTON,
199
+ actionSubjectId: ACTION_SUBJECT_ID.ELEMENT_DRAG_HANDLE,
200
+ attributes: {
201
+ nodeDepth: resolvedStartPos.depth,
202
+ nodeTypes: ((_resolvedStartPos$nod = resolvedStartPos.nodeAfter) === null || _resolvedStartPos$nod === void 0 ? void 0 : _resolvedStartPos$nod.type.name) || ''
203
+ },
204
+ eventType: EVENT_TYPE.UI
205
+ })(tr);
206
+ expandAndUpdateSelection({
207
+ api,
208
+ isShiftPressed: event.shiftKey,
209
+ nodeType,
210
+ selection,
211
+ startPos,
212
+ tr
213
+ });
214
+ api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : _api$blockControls2.commands.startPreservingSelection()({
215
+ tr
216
+ });
217
+ api === null || api === void 0 ? void 0 : (_api$blockControls3 = api.blockControls) === null || _api$blockControls3 === void 0 ? void 0 : _api$blockControls3.commands.toggleBlockMenu({
218
+ anchorName: getAnchorName(),
219
+ openedViaKeyboard,
220
+ triggerByNode: {
221
+ nodeType,
222
+ pos: startPos,
223
+ rootPos: (_activeNode$rootPos = activeNode === null || activeNode === void 0 ? void 0 : activeNode.rootPos) !== null && _activeNode$rootPos !== void 0 ? _activeNode$rootPos : resolvedStartPos.before(1)
224
+ }
225
+ })({
226
+ tr
227
+ });
228
+ tr.setMeta('scrollIntoView', false);
229
+ return tr;
230
+ });
231
+ view.focus();
232
+ }, [activeNode === null || activeNode === void 0 ? void 0 : activeNode.rootPos, api, getAnchorName, getPos, isDisabled, nodeType, view]);
233
+ const label = formatMessage(blockControlsMessages.dragToMoveClickToOpen, {
234
+ br: ' '
235
+ });
236
+ const helpDescriptors = useMemo(() => [{
237
+ description: formatMessage(blockControlsMessages.dragToMoveClickToOpen, {
238
+ br: /*#__PURE__*/React.createElement("br", null)
239
+ })
240
+ }], [formatMessage]);
241
+ const handleTooltipShow = useCallback(() => {
242
+ var _api$accessibilityUti;
243
+ api === null || api === void 0 ? void 0 : (_api$accessibilityUti = api.accessibilityUtils) === null || _api$accessibilityUti === void 0 ? void 0 : _api$accessibilityUti.actions.ariaNotify(label, {
244
+ priority: 'important'
245
+ });
246
+ }, [api, label]);
247
+ if (!view || !activeNode || pos === undefined) {
248
+ return null;
249
+ }
250
+ const tooltipContent = /*#__PURE__*/React.createElement(TooltipContentWithMultipleShortcuts, {
251
+ helpDescriptors: helpDescriptors
252
+ });
253
+ const browser = getBrowserInfo();
254
+ const button = /*#__PURE__*/React.createElement("button", {
255
+ ref: buttonRef,
256
+ type: "button",
257
+ "aria-label": label,
258
+ disabled: isDisabled,
259
+ onClick: handleClick,
260
+ onMouseDown: handleMouseDown
261
+ // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop -- Pragmatic DnD owns this native drag source.
262
+ ,
263
+ onDrop: event => event.stopPropagation(),
264
+ "data-editor-block-ctrl-drag-handle": true,
265
+ "data-blocks-drag-handle": fg('confluence_remix_button_right_side_block_fg') || undefined,
266
+ "data-testid": "block-controls-surface-drag-handle",
267
+ className: ax([dragHandleStyles.root, browser.gecko && dragHandleStyles.firefoxSelection, isLayoutColumn && dragHandleStyles.layoutColumn, dragHandleSelected && interactionState !== 'hasNotHadInteraction' && dragHandleStyles.selected, (handleOptions === null || handleOptions === void 0 ? void 0 : handleOptions.isFocused) && dragHandleStyles.keyboardFocused, dragHandleStyles.scaled])
268
+ }, /*#__PURE__*/React.createElement("span", {
269
+ onDragStart: handleIconDragStart,
270
+ className: ax([dragHandleStyles.iconWrapper])
271
+ }, shouldUseNestedDragHandleIcon(Boolean(isTopLevelNode), isLayoutColumn) ? /*#__PURE__*/React.createElement(DragHandleNestedIcon, null) : /*#__PURE__*/React.createElement(DragHandleVerticalIcon, {
272
+ label: "",
273
+ size: "small",
274
+ spacing: "spacious"
275
+ })));
276
+ return /*#__PURE__*/React.createElement(Tooltip, {
277
+ position: "top",
278
+ content: tooltipContent,
279
+ ignoreTooltipPointerEvents: true,
280
+ onShow: handleTooltipShow
281
+ }, button);
282
+ };
@@ -1,3 +1,4 @@
1
- ._1e0c1txw{display:flex}
1
+ ._zulpv77o{gap:var(--ds-space-025,2px)}._1e0c1txw{display:flex}
2
2
  ._1pbyb4wl{z-index:100}
3
+ ._4cvr1h6o{align-items:center}
3
4
  ._kqswstnw{position:absolute}
@@ -16,21 +16,17 @@ import { getAbsoluteSurfacePlacement, getSurfacePlacement } from './utils/get-su
16
16
  import { VisibilityContainer } from './visibility-container';
17
17
  const BLOCK_CONTROLS_SURFACE_Z_INDEX = 100;
18
18
  const surfaceStyles = null;
19
- export const BlockControlsSurface = ({
19
+ const BlockControlsSurfaceAtTarget = ({
20
+ activeNode,
20
21
  api,
21
- editorView
22
+ editorView,
23
+ target
22
24
  }) => {
23
- var _api$uiControlRegistr, _api$uiControlRegistr2;
24
- const activeNode = useSharedPluginStateWithSelector(api, ['blockControls'], states => {
25
- var _states$blockControls;
26
- return {
27
- activeNode: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.activeNode
28
- };
29
- }).activeNode;
25
+ var _api$uiControlRegistr, _api$uiControlRegistr2, _activeNode$handleOpt;
30
26
  const [position, setPosition] = useState();
31
27
  const animationFrameRef = useRef(undefined);
32
28
  const components = (_api$uiControlRegistr = (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(BLOCK_CONTROLS_LEFT_SURFACE)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
33
- const blockControlsContext = useMemo(() => createBlockControlsSurfaceContext(editorView, activeNode), [activeNode, editorView]);
29
+ const blockControlsContext = useMemo(() => createBlockControlsSurfaceContext(editorView, activeNode, target), [activeNode, editorView, target]);
34
30
  const surfaceContext = useMemo(() => blockControlsContext ? createSurfaceContext(BLOCK_CONTROL_UI_CONTEXT, blockControlsContext) : undefined, [blockControlsContext]);
35
31
  useLayoutEffect(() => {
36
32
  var _getDocument;
@@ -38,29 +34,29 @@ export const BlockControlsSurface = ({
38
34
  setPosition(undefined);
39
35
  return;
40
36
  }
41
- const rootNodeElement = editorView.nodeDOM(blockControlsContext.rootNode.pos);
42
- const rootElement = rootNodeElement instanceof HTMLElement ? rootNodeElement : rootNodeElement === null || rootNodeElement === void 0 ? void 0 : rootNodeElement.parentElement;
43
- if (!rootElement) {
37
+ const targetNodeElement = editorView.nodeDOM(blockControlsContext.targetNode.pos);
38
+ const targetElementRoot = targetNodeElement instanceof HTMLElement ? targetNodeElement : targetNodeElement === null || targetNodeElement === void 0 ? void 0 : targetNodeElement.parentElement;
39
+ if (!targetElementRoot) {
44
40
  setPosition(undefined);
45
41
  return;
46
42
  }
47
- const targetElement = getNodeContentElement(rootElement, blockControlsContext.rootNode.type.name);
43
+ const targetElement = getNodeContentElement(targetElementRoot, blockControlsContext.targetNode.type.name);
48
44
  const offsetParent = editorView.dom.offsetParent;
49
45
  const isDocumentOffsetParent = !offsetParent || offsetParent === ((_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.body);
50
46
  const scrollingOffsetParent = offsetParent instanceof HTMLElement && !isDocumentOffsetParent ? offsetParent : undefined;
51
47
  const updatePosition = () => {
52
- var _rootElement$getAttri, _scrollingOffsetParen, _scrollingOffsetParen2;
48
+ var _targetElementRoot$ge, _scrollingOffsetParen, _scrollingOffsetParen2;
53
49
  animationFrameRef.current = undefined;
54
- if (!rootElement.isConnected || !targetElement.isConnected) {
50
+ if (!targetElementRoot.isConnected || !targetElement.isConnected) {
55
51
  setPosition(undefined);
56
52
  return;
57
53
  }
58
54
  const placement = getSurfacePlacement({
59
- layout: (_rootElement$getAttri = rootElement.getAttribute('layout')) !== null && _rootElement$getAttri !== void 0 ? _rootElement$getAttri : '',
55
+ layout: (_targetElementRoot$ge = targetElementRoot.getAttribute('layout')) !== null && _targetElementRoot$ge !== void 0 ? _targetElementRoot$ge : '',
60
56
  nodeRect: targetElement.getBoundingClientRect(),
61
- nodeType: blockControlsContext.rootNode.type.name,
62
- nodeTypeWithLevel: getNodeTypeWithLevel(blockControlsContext.rootNode.node),
63
- parentNodeType: blockControlsContext.rootNode.parentType === 'doc' ? undefined : blockControlsContext.rootNode.parentType
57
+ nodeType: blockControlsContext.targetNode.type.name,
58
+ nodeTypeWithLevel: getNodeTypeWithLevel(blockControlsContext.targetNode.node),
59
+ parentNodeType: blockControlsContext.targetNode.parentType === 'doc' ? undefined : blockControlsContext.targetNode.parentType
64
60
  });
65
61
  const offsetParentRect = offsetParent === null || offsetParent === void 0 ? void 0 : offsetParent.getBoundingClientRect();
66
62
  const offsetParentScrollTop = (_scrollingOffsetParen = scrollingOffsetParent === null || scrollingOffsetParent === void 0 ? void 0 : scrollingOffsetParent.scrollTop) !== null && _scrollingOffsetParen !== void 0 ? _scrollingOffsetParen : window.scrollY;
@@ -128,17 +124,47 @@ export const BlockControlsSurface = ({
128
124
  return null;
129
125
  }
130
126
  return /*#__PURE__*/React.createElement("div", {
127
+ "data-editor-block-controls-surface": true
131
128
  // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Placement is resolved from the active node's runtime DOM geometry.
129
+ ,
132
130
  style: position,
133
- "data-testid": "block-controls-left-surface",
134
- className: ax(["_1e0c1txw _kqswstnw _1pbyb4wl"])
131
+ "data-testid": target === 'root' ? 'block-controls-left-surface' : 'block-controls-left-surface-active',
132
+ className: ax(["_zulpv77o _4cvr1h6o _1e0c1txw _kqswstnw _1pbyb4wl"])
135
133
  }, /*#__PURE__*/React.createElement(VisibilityContainer, {
136
134
  api: api,
137
135
  controlSide: "left",
136
+ forceVisibleOnMouseOut: Boolean((_activeNode$handleOpt = activeNode.handleOptions) === null || _activeNode$handleOpt === void 0 ? void 0 : _activeNode$handleOpt.isFocused),
138
137
  shouldUseDisplayContents: true
139
138
  }, /*#__PURE__*/React.createElement(SurfaceRenderer, {
140
139
  components: components,
141
140
  surface: BLOCK_CONTROLS_LEFT_SURFACE,
142
141
  surfaceContext: surfaceContext
143
142
  })));
143
+ };
144
+ export const BlockControlsSurface = ({
145
+ api,
146
+ editorView
147
+ }) => {
148
+ var _activeNode$rootPos;
149
+ const activeNode = useSharedPluginStateWithSelector(api, ['blockControls'], states => {
150
+ var _states$blockControls;
151
+ return {
152
+ activeNode: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.activeNode
153
+ };
154
+ }).activeNode;
155
+ if (!activeNode) {
156
+ return null;
157
+ }
158
+ const rootPos = (_activeNode$rootPos = activeNode.rootPos) !== null && _activeNode$rootPos !== void 0 ? _activeNode$rootPos : activeNode.pos;
159
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(BlockControlsSurfaceAtTarget, {
160
+ activeNode: activeNode,
161
+ api: api,
162
+ editorView: editorView,
163
+ target: "root"
164
+ }), rootPos !== activeNode.pos ? /*#__PURE__*/React.createElement(BlockControlsSurfaceAtTarget, {
165
+ activeNode: activeNode,
166
+ api: api,
167
+ editorView: editorView,
168
+ target: "active"
169
+ }) : null);
144
170
  };
@@ -7,6 +7,7 @@ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equ
7
7
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
8
8
  export const ACTIVE_DRAG_HANDLE_ATTR = 'data-active-drag-handle';
9
9
  export const ACTIVE_QUICK_INSERT_ATTR = 'data-active-quick-insert';
10
+ export const BLOCK_CONTROLS_SURFACE_SELECTOR = '[data-editor-block-controls-surface]';
10
11
  export const ACTIVE_DRAG_HANDLE_FALLBACK_ANCHOR_NAME = '--editor-block-controls-active-drag-handle-anchor';
11
12
  export const ACTIVE_QUICK_INSERT_FALLBACK_ANCHOR_NAME = '--editor-block-controls-active-quick-insert-anchor';
12
13
  export const DRAG_HANDLE_HEIGHT = 24;
@@ -0,0 +1,4 @@
1
+ import { createContext, useContext } from 'react';
2
+ /** Provides registered block-control surface contributors with their owning editor view. */
3
+ export const SurfaceEditorViewContext = /*#__PURE__*/createContext(undefined);
4
+ export const useSurfaceEditorView = () => useContext(SurfaceEditorViewContext);
@@ -0,0 +1,170 @@
1
+ import { useEffect } from 'react';
2
+ import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
3
+ import { startMeasure } from '@atlaskit/editor-common/performance-measures';
4
+ import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
5
+ import { draggable } from '@atlaskit/pragmatic-drag-and-drop/adapter/element-adapter';
6
+ import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/utils/set-custom-native-drag-preview';
7
+ import { defaultActiveAnchorTracker } from '../pm-plugins/utils/active-anchor-tracker';
8
+ import { getMultiSelectAnalyticsAttributes } from '../pm-plugins/utils/analytics';
9
+ import { isHandleCorrelatedToSelection, selectNode } from '../pm-plugins/utils/getSelection';
10
+ import { dragPreview } from './drag-preview';
11
+ import { getNodeMarginsForDragPreview, getNodeSpacingForDragPreview } from './block-controls-surface-drag-handle-utils';
12
+ const EDITOR_BLOCKS_DRAG_INIT = 'Editor Blocks Drag Initialization Time';
13
+ /**
14
+ * Registry surface drag source. This intentionally duplicates the stable widget handle's drag
15
+ * contract so the legacy implementation remains byte-for-byte unchanged during migration.
16
+ */
17
+ export const useBlockControlsSurfaceDragSource = ({
18
+ api,
19
+ elementRef,
20
+ getAnchorName,
21
+ getPos,
22
+ nodeType,
23
+ start,
24
+ view
25
+ }) => {
26
+ useEffect(() => {
27
+ const element = elementRef.current;
28
+ if (!element || !view) {
29
+ return;
30
+ }
31
+ return draggable({
32
+ element,
33
+ getInitialData: () => ({
34
+ type: 'element',
35
+ start
36
+ }),
37
+ onGenerateDragPreview: ({
38
+ nativeSetDragImage
39
+ }) => {
40
+ var _api$core, _api$blockControls$sh;
41
+ api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
42
+ tr
43
+ }) => {
44
+ const handlePos = getPos();
45
+ if (typeof handlePos !== 'number') {
46
+ return tr;
47
+ }
48
+ if (!tr.selection.empty && isHandleCorrelatedToSelection(view.state, tr.selection, handlePos)) {
49
+ var _api$blockControls;
50
+ api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.commands.setMultiSelectPositions()({
51
+ tr
52
+ });
53
+ } else {
54
+ tr = selectNode(tr, handlePos, nodeType, api);
55
+ }
56
+ return tr;
57
+ });
58
+ const startPos = getPos();
59
+ const {
60
+ doc,
61
+ selection
62
+ } = view.state;
63
+ let sliceFrom = selection.from;
64
+ let sliceTo = selection.to;
65
+ const multiSelect = 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;
66
+ if (multiSelect) {
67
+ sliceFrom = Math.min(multiSelect.anchor, multiSelect.head);
68
+ sliceTo = Math.max(multiSelect.anchor, multiSelect.head);
69
+ }
70
+ const expandedSlice = doc.slice(sliceFrom, sliceTo);
71
+ const isDraggingMultipleNodes = startPos !== undefined && startPos >= sliceFrom && startPos < sliceTo && expandedSlice.content.childCount > 1;
72
+ setCustomNativeDragPreview({
73
+ getOffset: () => {
74
+ if (!isDraggingMultipleNodes || startPos === undefined) {
75
+ return {
76
+ x: 0,
77
+ y: 0
78
+ };
79
+ }
80
+ const domAtPos = view.domAtPos.bind(view);
81
+ let heightBeforeHandle = 0;
82
+ let nodeStart = sliceFrom;
83
+ let activeNodeMarginTop = 0;
84
+ for (let index = 0; index < expandedSlice.content.childCount; index++) {
85
+ var _node$nodeSize;
86
+ const node = expandedSlice.content.maybeChild(index);
87
+ const nodeEnd = nodeStart + ((_node$nodeSize = node === null || node === void 0 ? void 0 : node.nodeSize) !== null && _node$nodeSize !== void 0 ? _node$nodeSize : 0);
88
+ if (nodeEnd <= startPos) {
89
+ const nodeElement = findDomRefAtPos(nodeStart, domAtPos);
90
+ const margins = getNodeMarginsForDragPreview(node);
91
+ heightBeforeHandle += (nodeElement instanceof HTMLElement ? nodeElement.offsetHeight : 0) + margins.top + margins.bottom;
92
+ } else {
93
+ activeNodeMarginTop = getNodeMarginsForDragPreview(node).top;
94
+ break;
95
+ }
96
+ nodeStart = nodeEnd;
97
+ }
98
+ return {
99
+ x: 0,
100
+ y: heightBeforeHandle + activeNodeMarginTop
101
+ };
102
+ },
103
+ render: ({
104
+ container
105
+ }) => {
106
+ if (!isDraggingMultipleNodes && startPos !== undefined) {
107
+ const dom = findDomRefAtPos(startPos, view.domAtPos.bind(view));
108
+ return dom instanceof HTMLElement ? dragPreview(container, {
109
+ dom,
110
+ nodeType
111
+ }) : undefined;
112
+ }
113
+ const previewContent = [];
114
+ expandedSlice.content.descendants((node, pos) => {
115
+ const dom = findDomRefAtPos(sliceFrom + pos, view.domAtPos.bind(view));
116
+ if (dom instanceof HTMLElement) {
117
+ previewContent.push({
118
+ dom,
119
+ nodeSpacing: getNodeSpacingForDragPreview(node),
120
+ nodeType: node.type.name
121
+ });
122
+ }
123
+ return false;
124
+ });
125
+ return previewContent.length > 0 ? dragPreview(container, previewContent) : undefined;
126
+ },
127
+ nativeSetDragImage
128
+ });
129
+ },
130
+ onDragStart: () => {
131
+ var _api$core2;
132
+ if (start === undefined) {
133
+ return;
134
+ }
135
+
136
+ // This handle is outside ProseMirror, so it owns the side effects normally started by PM.
137
+ startMeasure(EDITOR_BLOCKS_DRAG_INIT);
138
+ defaultActiveAnchorTracker.reset();
139
+ api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
140
+ tr
141
+ }) => {
142
+ var _api$blockControls$sh2, _resolvedMovingNode$n, _api$blockControls2, _api$analytics;
143
+ const resolvedMovingNode = tr.doc.resolve(start);
144
+ const multiSelect = 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;
145
+ const attributes = multiSelect ? getMultiSelectAnalyticsAttributes(tr, multiSelect.anchor, multiSelect.head) : {
146
+ hasSelectedMultipleNodes: false,
147
+ nodeTypes: (_resolvedMovingNode$n = resolvedMovingNode.nodeAfter) === null || _resolvedMovingNode$n === void 0 ? void 0 : _resolvedMovingNode$n.type.name
148
+ };
149
+ api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : _api$blockControls2.commands.setNodeDragged(getPos, getAnchorName(), nodeType)({
150
+ tr
151
+ });
152
+ tr.setMeta('scrollIntoView', false);
153
+ api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.attachAnalyticsEvent({
154
+ action: ACTION.DRAGGED,
155
+ actionSubject: ACTION_SUBJECT.ELEMENT,
156
+ actionSubjectId: ACTION_SUBJECT_ID.ELEMENT_DRAG_HANDLE,
157
+ attributes: {
158
+ hasSelectedMultipleNodes: attributes.hasSelectedMultipleNodes,
159
+ nodeDepth: resolvedMovingNode.depth,
160
+ nodeTypes: attributes.nodeTypes || ''
161
+ },
162
+ eventType: EVENT_TYPE.UI
163
+ })(tr);
164
+ return tr;
165
+ });
166
+ view.focus();
167
+ }
168
+ });
169
+ }, [api, elementRef, getAnchorName, getPos, nodeType, start, view]);
170
+ };
@@ -1,4 +1,4 @@
1
- import { QUICK_INSERT_LEFT_OFFSET, dragHandleGap, topPositionAdjustment } from '../consts';
1
+ import { dragHandleGap, topPositionAdjustment } from '../consts';
2
2
  export const getAbsoluteSurfacePlacement = ({
3
3
  offsetParentRect,
4
4
  offsetParentScrollLeft,
@@ -23,8 +23,17 @@ export const getSurfacePlacement = ({
23
23
  nodeType,
24
24
  nodeTypeWithLevel,
25
25
  parentNodeType
26
- }) => ({
27
- left: nodeRect.left,
28
- top: nodeRect.top + topPositionAdjustment(nodeTypeWithLevel, layout),
29
- transform: `translateX(calc(-100% - ${dragHandleGap(nodeType, parentNodeType) + QUICK_INSERT_LEFT_OFFSET}px))`
30
- });
26
+ }) => {
27
+ if (nodeType === 'layoutColumn') {
28
+ return {
29
+ left: nodeRect.left + nodeRect.width / 2,
30
+ top: nodeRect.top,
31
+ transform: 'translate(-50%, -50%)'
32
+ };
33
+ }
34
+ return {
35
+ left: nodeRect.left,
36
+ top: nodeRect.top + topPositionAdjustment(nodeTypeWithLevel, layout),
37
+ transform: `translateX(calc(-100% - ${dragHandleGap(nodeType, parentNodeType)}px))`
38
+ };
39
+ };