@atlaskit/editor-plugin-block-controls 24.0.0 → 24.0.1

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 (25) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/cjs/pm-plugins/{handle-sparse-mouse-over.js → handle-sparse-hover-target.js} +30 -17
  3. package/dist/cjs/pm-plugins/main.js +7 -7
  4. package/dist/cjs/pm-plugins/sparse-gutter-hover.js +317 -0
  5. package/dist/cjs/pm-plugins/{utils/sparse-surfaces-view.js → sparse-surfaces-view.js} +10 -4
  6. package/dist/cjs/pm-plugins/sparse-surfaces.js +1 -1
  7. package/dist/cjs/pm-plugins/utils/visible-box.js +27 -0
  8. package/dist/es2019/pm-plugins/{handle-sparse-mouse-over.js → handle-sparse-hover-target.js} +29 -16
  9. package/dist/es2019/pm-plugins/main.js +7 -7
  10. package/dist/es2019/pm-plugins/sparse-gutter-hover.js +305 -0
  11. package/dist/es2019/pm-plugins/{utils/sparse-surfaces-view.js → sparse-surfaces-view.js} +10 -4
  12. package/dist/es2019/pm-plugins/sparse-surfaces.js +1 -1
  13. package/dist/es2019/pm-plugins/utils/visible-box.js +21 -0
  14. package/dist/esm/pm-plugins/{handle-sparse-mouse-over.js → handle-sparse-hover-target.js} +29 -16
  15. package/dist/esm/pm-plugins/main.js +7 -7
  16. package/dist/esm/pm-plugins/sparse-gutter-hover.js +311 -0
  17. package/dist/esm/pm-plugins/{utils/sparse-surfaces-view.js → sparse-surfaces-view.js} +10 -4
  18. package/dist/esm/pm-plugins/sparse-surfaces.js +1 -1
  19. package/dist/esm/pm-plugins/utils/visible-box.js +21 -0
  20. package/dist/types/pm-plugins/handle-sparse-hover-target.d.ts +10 -0
  21. package/dist/types/pm-plugins/sparse-gutter-hover.d.ts +27 -0
  22. package/dist/types/pm-plugins/{utils/sparse-surfaces-view.d.ts → sparse-surfaces-view.d.ts} +2 -2
  23. package/dist/types/pm-plugins/utils/visible-box.d.ts +5 -0
  24. package/package.json +4 -4
  25. package/dist/types/pm-plugins/handle-sparse-mouse-over.d.ts +0 -5
@@ -29,7 +29,6 @@ import { getActiveDropTargetDecorations } from './decorations-drop-target-active
29
29
  import { createActiveQuickInsertNodeDecoration, findActiveQuickInsertNodeDec, findQuickInsertInsertButtonDecoration, quickInsertButtonDecoration, TYPE_ACTIVE_QUICK_INSERT_NODE } from './decorations-quick-insert-button';
30
30
  import { handleMouseDown } from './handle-mouse-down';
31
31
  import { handleMouseOver } from './handle-mouse-over';
32
- import { handleSparseMouseOver } from './handle-sparse-mouse-over';
33
32
  import { boundKeydownHandler } from './keymap';
34
33
  import { defaultActiveAnchorTracker } from './utils/active-anchor-tracker';
35
34
  import { getMultiSelectAnalyticsAttributes } from './utils/analytics';
@@ -1073,6 +1072,11 @@ export const createPlugin = (api, getIntl, nodeViewPortalProviderAPI, nodeDecora
1073
1072
  },
1074
1073
  mouseover: (view, event) => {
1075
1074
  var _api$limitedMode7, _api$limitedMode7$sha, _getNodeIdProvider;
1075
+ // Sparse surfaces handle all hover in bindSparseGutterHover, which also covers the gutters
1076
+ // outside view.dom.
1077
+ if (isExperimentEnabled('platform_editor_block_control_migration')) {
1078
+ return false;
1079
+ }
1076
1080
  if (api !== null && api !== void 0 && (_api$limitedMode7 = api.limitedMode) !== null && _api$limitedMode7 !== void 0 && (_api$limitedMode7$sha = _api$limitedMode7.sharedState.currentState()) !== null && _api$limitedMode7$sha !== void 0 && _api$limitedMode7$sha.enabled) {
1077
1081
  return;
1078
1082
  }
@@ -1080,14 +1084,10 @@ export const createPlugin = (api, getIntl, nodeViewPortalProviderAPI, nodeDecora
1080
1084
  // another layer of protection to prevent handling mouseover
1081
1085
  // when native anchor with dnd is enabled in limited mode
1082
1086
  // in case there are descripancies between getNodeIdProvider limited mode state
1083
- if ((_getNodeIdProvider = getNodeIdProvider(view)) !== null && _getNodeIdProvider !== void 0 && _getNodeIdProvider.isLimitedMode() && (expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) || isExperimentEnabled('platform_editor_block_control_migration'))) {
1087
+ if ((_getNodeIdProvider = getNodeIdProvider(view)) !== null && _getNodeIdProvider !== void 0 && _getNodeIdProvider.isLimitedMode() && expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) {
1084
1088
  return;
1085
1089
  }
1086
- if (isExperimentEnabled('platform_editor_block_control_migration')) {
1087
- handleSparseMouseOver(view, event, api);
1088
- } else {
1089
- handleMouseOver(view, event, api);
1090
- }
1090
+ handleMouseOver(view, event, api);
1091
1091
  return false;
1092
1092
  },
1093
1093
  mousedown: (view, event) => {
@@ -0,0 +1,305 @@
1
+ import { bind } from 'bind-event-listener';
2
+ import { DRAG_HANDLE_WIDTH } from '@atlaskit/editor-common/styles';
3
+ import { akEditorFullPageNarrowBreakout, akEditorGutterPaddingDynamic, akEditorGutterPaddingReduced } from '@atlaskit/editor-shared-styles/constants';
4
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
5
+ import { BLOCK_CONTROLS_SURFACE_SELECTOR, DRAG_HANDLE_MAX_WIDTH_PLUS_GAP } from '../ui/consts';
6
+ import { handleSparseHoverTarget, isSparseHoverSuppressed } from './handle-sparse-hover-target';
7
+ import { mouseEnter, setHoverSide, stopEditing } from './interaction-tracking/commands';
8
+ import { handleMouseLeave } from './interaction-tracking/handle-mouse-move';
9
+ import { getInteractionTrackingState } from './interaction-tracking/pm-plugin';
10
+ import { getVisibleBox } from './utils/visible-box';
11
+ const BLOCK_SELECTOR = '[data-prosemirror-node-block="true"]';
12
+ const TABLE_SELECTOR = '[data-prosemirror-node-name="table"]';
13
+ const LAYOUT_SECTION_SELECTOR = '[data-prosemirror-node-name="layoutSection"]';
14
+ const LAYOUT_COLUMN_SELECTOR = '[data-prosemirror-node-name="layoutColumn"]';
15
+
16
+ // `layoutColumnExtendedHoverZone` is a DRAG_HANDLE_WIDTH-tall strip centred on the column's top edge.
17
+ const LAYOUT_COLUMN_STRIP_REACH = DRAG_HANDLE_WIDTH / 2;
18
+ const isBlockElement = element => element.getAttribute('data-prosemirror-node-block') === 'true';
19
+ const getVisibleLeft = block => {
20
+ var _block$getAttribute;
21
+ return getVisibleBox(block, (_block$getAttribute = block.getAttribute('data-prosemirror-node-name')) !== null && _block$getAttribute !== void 0 ? _block$getAttribute : '').getBoundingClientRect().left;
22
+ };
23
+
24
+ /**
25
+ * Whether the element under the pointer can be part of a block's left gutter: a container block's
26
+ * own padding (panel, layout, list…) or an editor wrapper around the content. Text and leaf blocks
27
+ * have no gutter inside them, and neither do tables: like the legacy hover zones, anywhere inside a
28
+ * table, including cell padding, shows the table's controls. Elements rendered beside the editor
29
+ * (title, popups, toolbars) cover any gutter beneath them.
30
+ */
31
+ export const isPossibleGutterElement = (view, hoveredElement) => {
32
+ if (!(hoveredElement instanceof Element)) {
33
+ return false;
34
+ }
35
+ const owner = hoveredElement.closest(`${BLOCK_SELECTOR}, ${BLOCK_CONTROLS_SURFACE_SELECTOR}`);
36
+ if (owner !== null && owner !== void 0 && owner.matches(BLOCK_CONTROLS_SURFACE_SELECTOR)) {
37
+ return false;
38
+ }
39
+ if (owner && view.dom.contains(owner)) {
40
+ var _owner$getAttribute;
41
+ if (owner.closest(TABLE_SELECTOR)) {
42
+ return false;
43
+ }
44
+ const nodeType = view.state.schema.nodes[(_owner$getAttribute = owner.getAttribute('data-prosemirror-node-name')) !== null && _owner$getAttribute !== void 0 ? _owner$getAttribute : ''];
45
+ return !(nodeType !== null && nodeType !== void 0 && nodeType.isTextblock) && !(nodeType !== null && nodeType !== void 0 && nodeType.isLeaf);
46
+ }
47
+ return view.dom.contains(hoveredElement) || hoveredElement.contains(view.dom);
48
+ };
49
+ const getGutterWidths = () => ({
50
+ topLevel: akEditorGutterPaddingDynamic(),
51
+ narrowTopLevel: expValEqualsNoExposure('platform_editor_preview_panel_responsiveness', 'isEnabled', true) ? akEditorGutterPaddingReduced : undefined
52
+ });
53
+ const getTopLevelGutterWidth = (api, widths) => {
54
+ var _api$width, _api$width$sharedStat;
55
+ const editorWidth = api === null || api === void 0 ? void 0 : (_api$width = api.width) === null || _api$width === void 0 ? void 0 : (_api$width$sharedStat = _api$width.sharedState.currentState()) === null || _api$width$sharedStat === void 0 ? void 0 : _api$width$sharedStat.width;
56
+ return widths.narrowTopLevel !== undefined && editorWidth !== undefined && editorWidth <= akEditorFullPageNarrowBreakout ? widths.narrowTopLevel : widths.topLevel;
57
+ };
58
+ const getEditorElementAt = (view, clientX, clientY) => {
59
+ const element = view.dom.ownerDocument.elementFromPoint(clientX, clientY);
60
+ return element && element !== view.dom && view.dom.contains(element) ? element : null;
61
+ };
62
+
63
+ /** The outermost block around `element` that does not contain the pointer owns that gutter. */
64
+ const findGutterOwner = (view, element, pointerTarget) => {
65
+ let owner = null;
66
+ for (let current = element; current && current !== view.dom;) {
67
+ if (isBlockElement(current) && !current.contains(pointerTarget) &&
68
+ // Layout columns have a top hover strip instead of a left gutter.
69
+ current.getAttribute('data-prosemirror-node-name') !== 'layoutColumn') {
70
+ owner = current;
71
+ }
72
+ current = current.parentElement;
73
+ }
74
+ return owner;
75
+ };
76
+ const findBlockBesidePointer = (view, api, event, pointerTarget, widths) => {
77
+ const pointerBlock = pointerTarget.closest(BLOCK_SELECTOR);
78
+ const gutterWidth = pointerBlock && view.dom.contains(pointerBlock) ? DRAG_HANDLE_MAX_WIDTH_PLUS_GAP : getTopLevelGutterWidth(api, widths);
79
+ const sample = getEditorElementAt(view, event.clientX + gutterWidth, event.clientY);
80
+ const owner = sample && findGutterOwner(view, sample, pointerTarget);
81
+ if (!owner) {
82
+ return null;
83
+ }
84
+ const left = getVisibleLeft(owner);
85
+ return event.clientX < left && left - event.clientX <= gutterWidth ? owner : null;
86
+ };
87
+ const findColumnBelowPointer = (view, event, pointerTarget) => {
88
+ if (pointerTarget.closest(LAYOUT_COLUMN_SELECTOR) || pointerTarget !== view.dom && !pointerTarget.closest(LAYOUT_SECTION_SELECTOR)) {
89
+ return null;
90
+ }
91
+ const sample = getEditorElementAt(view, event.clientX, event.clientY + LAYOUT_COLUMN_STRIP_REACH);
92
+ const column = sample === null || sample === void 0 ? void 0 : sample.closest(LAYOUT_COLUMN_SELECTOR);
93
+ return column && view.dom.contains(column) && event.clientY < column.getBoundingClientRect().top ? column : null;
94
+ };
95
+
96
+ /** Returns a function that finds the block whose legacy hover zone contains the pointer. */
97
+ export const createGutterBlockFinder = (view, api) => {
98
+ const widths = getGutterWidths();
99
+ return event => {
100
+ var _findBlockBesidePoint;
101
+ if (!(event.target instanceof Element)) {
102
+ return null;
103
+ }
104
+ return (_findBlockBesidePoint = findBlockBesidePointer(view, api, event, event.target, widths)) !== null && _findBlockBesidePoint !== void 0 ? _findBlockBesidePoint : view.dom.contains(event.target) ? findColumnBelowPointer(view, event, event.target) : null;
105
+ };
106
+ };
107
+
108
+ /** The latest gutter event waiting to be handled after the frame renders. */
109
+
110
+ /** What gutter hover does with a pointer position. */
111
+
112
+ /**
113
+ * Leaving block controls does not count as reaching a new element: a nested block's drag handle
114
+ * overhangs the block, so leaving the handle can put the pointer on the container with no block
115
+ * beside it, and that exit must keep the controls on the block.
116
+ */
117
+ const isLeavingBlockControls = event => event.relatedTarget instanceof Element && Boolean(event.relatedTarget.closest(BLOCK_CONTROLS_SURFACE_SELECTOR));
118
+ const classifyGutterHover = (view, findGutterBlock, {
119
+ event,
120
+ reachedNewElement
121
+ }) => {
122
+ const block = findGutterBlock(event);
123
+ if (block) {
124
+ return {
125
+ kind: 'block',
126
+ element: block
127
+ };
128
+ }
129
+ const hoveredElement = event.target;
130
+ if (reachedNewElement && hoveredElement instanceof Element && view.dom.contains(hoveredElement)) {
131
+ return {
132
+ kind: 'container',
133
+ element: hoveredElement
134
+ };
135
+ }
136
+ return {
137
+ kind: 'none'
138
+ };
139
+ };
140
+
141
+ /**
142
+ * Runs `callback` once after the next frame renders (`requestAnimationFrame`, then `setTimeout`).
143
+ * Layout is clean then, so reading positions never forces a synchronous layout. Calls made while a
144
+ * run is already scheduled are ignored.
145
+ */
146
+ const createAfterRenderScheduler = (win, callback) => {
147
+ let frame;
148
+ let timeout;
149
+ return {
150
+ schedule: () => {
151
+ if (frame !== undefined || timeout !== undefined) {
152
+ return;
153
+ }
154
+ frame = win.requestAnimationFrame(() => {
155
+ frame = undefined;
156
+ timeout = win.setTimeout(() => {
157
+ timeout = undefined;
158
+ callback();
159
+ }, 0);
160
+ });
161
+ },
162
+ cancel: () => {
163
+ if (frame !== undefined) {
164
+ win.cancelAnimationFrame(frame);
165
+ frame = undefined;
166
+ }
167
+ if (timeout !== undefined) {
168
+ win.clearTimeout(timeout);
169
+ timeout = undefined;
170
+ }
171
+ }
172
+ };
173
+ };
174
+
175
+ /**
176
+ * Keeps interaction tracking in step with gutter hover, as the legacy hover zones did. They sat
177
+ * inside the block tree, so hovering one cleared the typing state, counted as inside the editor even
178
+ * where the zone reached past the content area, and put the pointer on the left side of the
179
+ * right-side controls split.
180
+ */
181
+ const createInteractionTrackingSync = (view, api, contentArea) => {
182
+ // Set after gutter hover calls mouseEnter for a gutter outside the content area, so that leaving
183
+ // that gutter calls mouseLeave.
184
+ let isHoldingMouseEnter = false;
185
+ return (event, isOverGutter) => {
186
+ var _api$blockControls$sh;
187
+ const state = getInteractionTrackingState(view.state);
188
+ const rightSideControlsEnabled = 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.rightSideControlsEnabled;
189
+ const side = rightSideControlsEnabled ? 'left' : undefined;
190
+ if (isOverGutter && state !== null && state !== void 0 && state.isEditing) {
191
+ stopEditing(view);
192
+ }
193
+ if (event.target instanceof Node && contentArea !== null && contentArea !== void 0 && contentArea.contains(event.target)) {
194
+ isHoldingMouseEnter = false;
195
+ } else if (isOverGutter && state !== null && state !== void 0 && state.isMouseOut) {
196
+ isHoldingMouseEnter = true;
197
+ mouseEnter(view, side);
198
+ return;
199
+ } else if (!isOverGutter && isHoldingMouseEnter) {
200
+ isHoldingMouseEnter = false;
201
+ handleMouseLeave(view, rightSideControlsEnabled);
202
+ return;
203
+ }
204
+ if (isOverGutter && side && (state === null || state === void 0 ? void 0 : state.hoverSide) !== side) {
205
+ setHoverSide(view, side);
206
+ }
207
+ };
208
+ };
209
+
210
+ /**
211
+ * Shows block controls for the block the pointer hovers under sparse surfaces, including the empty
212
+ * space to a block's left (the gutter). This is the only hover handler for sparse surfaces.
213
+ *
214
+ * Reaching text or a leaf block shows its controls straight away. Over an element that can be part
215
+ * of a gutter, only the latest pointer event is kept, and it is resolved once after the frame
216
+ * renders so that reading positions never forces a layout.
217
+ *
218
+ * Returns an unbind function, or `undefined` when the editor's document has no window.
219
+ */
220
+ export const bindSparseGutterHover = ({
221
+ api,
222
+ view
223
+ }) => {
224
+ var _ref, _ref2, _view$dom$closest;
225
+ const win = view.dom.ownerDocument.defaultView;
226
+ if (!win) {
227
+ return undefined;
228
+ }
229
+ const contentArea = view.dom.closest('.ak-editor-content-area');
230
+ // A wide block's gutter can sit outside the padded content area. Editors without a scroll
231
+ // container or content area still get hover, over as much of the gutter as their wrapper covers.
232
+ const eventArea = (_ref = (_ref2 = (_view$dom$closest = view.dom.closest('[data-editor-scroll-container="true"]')) !== null && _view$dom$closest !== void 0 ? _view$dom$closest : contentArea) !== null && _ref2 !== void 0 ? _ref2 : view.dom.parentElement) !== null && _ref !== void 0 ? _ref : view.dom;
233
+ const findGutterBlock = createGutterBlockFinder(view, api);
234
+ const syncInteractionTracking = createInteractionTrackingSync(view, api, contentArea);
235
+ let pending;
236
+ const handlePendingPointer = () => {
237
+ const pointer = pending;
238
+ pending = undefined;
239
+ if (!pointer || isSparseHoverSuppressed(view, api)) {
240
+ return;
241
+ }
242
+ const hover = classifyGutterHover(view, findGutterBlock, pointer);
243
+ if (hover.kind !== 'none') {
244
+ handleSparseHoverTarget(view, hover.element, api);
245
+ }
246
+ syncInteractionTracking(pointer.event, hover.kind === 'block');
247
+ };
248
+ const scheduler = createAfterRenderScheduler(win, handlePendingPointer);
249
+ const queueGutterEvent = (event, reachedNewElement) => {
250
+ var _pending;
251
+ pending = {
252
+ event,
253
+ reachedNewElement: Boolean((_pending = pending) === null || _pending === void 0 ? void 0 : _pending.reachedNewElement) || reachedNewElement
254
+ };
255
+ scheduler.schedule();
256
+ };
257
+
258
+ // Classified on every mouseover and reused while the pointer moves within that element, so
259
+ // movement over text costs one comparison.
260
+ let hovered;
261
+ const classifyHoveredElement = element => {
262
+ hovered = {
263
+ element,
264
+ isPossibleGutter: isPossibleGutterElement(view, element)
265
+ };
266
+ return hovered.isPossibleGutter;
267
+ };
268
+ const onMouseOver = event => {
269
+ if (classifyHoveredElement(event.target)) {
270
+ queueGutterEvent(event, !isLeavingBlockControls(event));
271
+ return;
272
+ }
273
+ pending = undefined;
274
+ const hoveredElement = event.target;
275
+ if (hoveredElement instanceof Element && view.dom.contains(hoveredElement) && !isSparseHoverSuppressed(view, api)) {
276
+ handleSparseHoverTarget(view, hoveredElement, api);
277
+ }
278
+ };
279
+ const onMouseMove = event => {
280
+ var _hovered;
281
+ const isPossibleGutter = ((_hovered = hovered) === null || _hovered === void 0 ? void 0 : _hovered.element) === event.target ? hovered.isPossibleGutter : classifyHoveredElement(event.target);
282
+ if (isPossibleGutter) {
283
+ queueGutterEvent(event, false);
284
+ }
285
+ };
286
+ const bindMouseListener = (type, listener) => bind(eventArea, {
287
+ type,
288
+ listener: event => {
289
+ if (event instanceof win.MouseEvent) {
290
+ listener(event);
291
+ }
292
+ },
293
+ options: {
294
+ passive: true
295
+ }
296
+ });
297
+ const unbindMouseOver = bindMouseListener('mouseover', onMouseOver);
298
+ const unbindMouseMove = bindMouseListener('mousemove', onMouseMove);
299
+ return () => {
300
+ unbindMouseOver();
301
+ unbindMouseMove();
302
+ scheduler.cancel();
303
+ pending = undefined;
304
+ };
305
+ };
@@ -1,10 +1,11 @@
1
1
  import { BLOCK_CONTROLS_LEFT_SURFACE, BLOCK_CONTROLS_RIGHT_SURFACE } from '@atlaskit/editor-common/block-controls/surface-keys';
2
2
  import { getNodeIdProvider } from '@atlaskit/editor-common/node-anchor';
3
3
  import { resolveSurface } from '@atlaskit/editor-ui-control-model/surface-renderer';
4
- import { createSurfaceAnchorDecorations } from '../decorations-surface-anchor';
5
- import { key as blockControlsKey } from '../main';
6
- import { reconcileSparseSurfaceCandidates } from './sparse-surface-candidates';
7
- import { observeSurfaceViewport } from './surface-viewport';
4
+ import { createSurfaceAnchorDecorations } from './decorations-surface-anchor';
5
+ import { key as blockControlsKey } from './main';
6
+ import { bindSparseGutterHover } from './sparse-gutter-hover';
7
+ import { reconcileSparseSurfaceCandidates } from './utils/sparse-surface-candidates';
8
+ import { observeSurfaceViewport } from './utils/surface-viewport';
8
9
  const haveAnchorPositionsChanged = (positions, anchors) => positions.length !== anchors.size || positions.some(position => !anchors.has(position));
9
10
  export const createSparseSurfacesView = ({
10
11
  api,
@@ -95,6 +96,10 @@ export const createSparseSurfacesView = ({
95
96
  schedule();
96
97
  };
97
98
  const unsubscribe = api === null || api === void 0 ? void 0 : (_api$uiControlRegistr3 = api.uiControlRegistry) === null || _api$uiControlRegistr3 === void 0 ? void 0 : _api$uiControlRegistr3.actions.subscribe(invalidate);
99
+ const unbindGutterHover = bindSparseGutterHover({
100
+ api,
101
+ view
102
+ });
98
103
  const unsubscribers = [api === null || api === void 0 ? void 0 : (_api$editorDisabled = api.editorDisabled) === null || _api$editorDisabled === void 0 ? void 0 : _api$editorDisabled.sharedState.onChange(invalidate), api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.sharedState.onChange(invalidate), api === null || api === void 0 ? void 0 : (_api$limitedMode2 = api.limitedMode) === null || _api$limitedMode2 === void 0 ? void 0 : _api$limitedMode2.sharedState.onChange(invalidate), api === null || api === void 0 ? void 0 : (_api$showDiff = api.showDiff) === null || _api$showDiff === void 0 ? void 0 : _api$showDiff.sharedState.onChange(invalidate), api === null || api === void 0 ? void 0 : (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.sharedState.onChange(invalidate)];
99
104
  return {
100
105
  update(currentView, previousState) {
@@ -114,6 +119,7 @@ export const createSparseSurfacesView = ({
114
119
  win === null || win === void 0 ? void 0 : win.cancelAnimationFrame(frame);
115
120
  }
116
121
  observer.destroy();
122
+ unbindGutterHover === null || unbindGutterHover === void 0 ? void 0 : unbindGutterHover();
117
123
  unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
118
124
  unsubscribers.forEach(unsubscribe => unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe());
119
125
  }
@@ -3,9 +3,9 @@ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
3
3
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
4
4
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
5
5
  import { key as blockControlsKey } from './main';
6
+ import { createSparseSurfacesView } from './sparse-surfaces-view';
6
7
  import { emptySparseSurfaceCandidateState } from './utils/sparse-surface-candidates';
7
8
  import { applySparseSurfacesTransaction, createInitialSparseSurfacesState } from './utils/sparse-surfaces-state';
8
- import { createSparseSurfacesView } from './utils/sparse-surfaces-view';
9
9
  export const sparseSurfacesKey = new PluginKey('blockControlsSparseSurfaces');
10
10
 
11
11
  /** Owns sparse anchors independently of drag/drop decorations and their lifecycle. */
@@ -0,0 +1,21 @@
1
+ // Matches the anchor selectors in `ui/global-styles.tsx`. A table's `.pm-table-resizer-container`
2
+ // wraps its cells too, so only a direct child `.resizer-item` is the table's own box, not the
3
+ // resizer of media inside a cell.
4
+ const VISIBLE_BOX_SELECTORS = {
5
+ table: '.pm-table-resizer-container > .resizer-item',
6
+ mediaSingle: '.resizer-item',
7
+ embedCard: '.rich-media-item',
8
+ extension: '.extension-container[data-layout]',
9
+ bodiedExtension: '.extension-container[data-layout]',
10
+ multiBodiedExtension: '.extension-container[data-layout]',
11
+ blockCard: '.datasourceView-content-inner-wrap'
12
+ };
13
+
14
+ /**
15
+ * The box a block's controls anchor to under sparse surfaces. For some blocks this is an inner
16
+ * element, which can be wider than the block itself.
17
+ */
18
+ export const getVisibleBox = (dom, nodeType) => {
19
+ const selector = VISIBLE_BOX_SELECTORS[nodeType];
20
+ return selector && dom.querySelector(selector) || dom;
21
+ };
@@ -1,3 +1,4 @@
1
+ import { getNodeIdProvider } from '@atlaskit/editor-common/node-anchor';
1
2
  import { fg } from '@atlaskit/platform-feature-flags/fg';
2
3
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
3
4
  import { getNodeTypeWithLevel } from './decorations-common';
@@ -34,26 +35,38 @@ var redirectParagraphToWrappedMedia = function redirectParagraphToWrappedMedia(v
34
35
  };
35
36
  };
36
37
 
37
- /** Resolves hover from PM structure so the first hover can create its sparse candidate. */
38
- export var handleSparseMouseOver = function handleSparseMouseOver(view, event, api) {
39
- var _api$editorDisabled$s, _api$editorDisabled, _api$editorViewMode, _api$showDiff$sharedS, _api$showDiff, _api$userIntent, _$pos$nodeAfter, _controls$activeNode, _anchors$get, _anchors$get2;
38
+ /** Whether hovering must not show block controls under sparse surfaces. */
39
+ export var isSparseHoverSuppressed = function isSparseHoverSuppressed(view, api) {
40
+ var _api$limitedMode, _getNodeIdProvider, _api$typeAhead, _api$editorDisabled$s, _api$editorDisabled, _api$editorViewMode, _api$showDiff$sharedS, _api$showDiff, _api$userIntent;
41
+ if (api !== null && api !== void 0 && (_api$limitedMode = api.limitedMode) !== null && _api$limitedMode !== void 0 && (_api$limitedMode = _api$limitedMode.sharedState.currentState()) !== null && _api$limitedMode !== void 0 && _api$limitedMode.enabled || (_getNodeIdProvider = getNodeIdProvider(view)) !== null && _getNodeIdProvider !== void 0 && _getNodeIdProvider.isLimitedMode()) {
42
+ return true;
43
+ }
40
44
  var controls = api === null || api === void 0 ? void 0 : api.blockControls.sharedState.currentState();
41
- if (!api || controls !== null && controls !== void 0 && controls.isDragging || !(event.target instanceof Element) || event.target === view.dom) {
42
- return false;
45
+ if (controls !== null && controls !== void 0 && controls.isDragging || api !== null && api !== void 0 && (_api$typeAhead = api.typeAhead) !== null && _api$typeAhead !== void 0 && (_api$typeAhead = _api$typeAhead.sharedState.currentState()) !== null && _api$typeAhead !== void 0 && _api$typeAhead.isOpen) {
46
+ return true;
43
47
  }
44
- var editorDisabled = (_api$editorDisabled$s = (_api$editorDisabled = api.editorDisabled) === null || _api$editorDisabled === void 0 || (_api$editorDisabled = _api$editorDisabled.sharedState.currentState()) === null || _api$editorDisabled === void 0 ? void 0 : _api$editorDisabled.editorDisabled) !== null && _api$editorDisabled$s !== void 0 ? _api$editorDisabled$s : false;
45
- var editorViewMode = (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode;
48
+ var editorDisabled = (_api$editorDisabled$s = api === null || api === void 0 || (_api$editorDisabled = api.editorDisabled) === null || _api$editorDisabled === void 0 || (_api$editorDisabled = _api$editorDisabled.sharedState.currentState()) === null || _api$editorDisabled === void 0 ? void 0 : _api$editorDisabled.editorDisabled) !== null && _api$editorDisabled$s !== void 0 ? _api$editorDisabled$s : false;
49
+ var editorViewMode = api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode;
46
50
  if (editorDisabled && (editorViewMode !== 'view' || !(controls !== null && controls !== void 0 && controls.rightSideControlsEnabled))) {
47
- return false;
51
+ return true;
48
52
  }
49
- var isDisplayingDiff = (_api$showDiff$sharedS = (_api$showDiff = api.showDiff) === null || _api$showDiff === void 0 || (_api$showDiff = _api$showDiff.sharedState.currentState()) === null || _api$showDiff === void 0 ? void 0 : _api$showDiff.isDisplayingChanges) !== null && _api$showDiff$sharedS !== void 0 ? _api$showDiff$sharedS : false;
50
- var currentUserIntent = (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 || (_api$userIntent = _api$userIntent.sharedState.currentState()) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.currentUserIntent;
51
- if (isDisplayingDiff || currentUserIntent === 'reviewing') {
53
+ var isDisplayingDiff = (_api$showDiff$sharedS = api === null || api === void 0 || (_api$showDiff = api.showDiff) === null || _api$showDiff === void 0 || (_api$showDiff = _api$showDiff.sharedState.currentState()) === null || _api$showDiff === void 0 ? void 0 : _api$showDiff.isDisplayingChanges) !== null && _api$showDiff$sharedS !== void 0 ? _api$showDiff$sharedS : false;
54
+ return isDisplayingDiff || (api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 || (_api$userIntent = _api$userIntent.sharedState.currentState()) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.currentUserIntent) === 'reviewing';
55
+ };
56
+
57
+ /**
58
+ * Shows controls for the block that owns `target`, resolved from PM structure so the first hover
59
+ * can create its sparse candidate. Callers check `isSparseHoverSuppressed` first.
60
+ */
61
+ export var handleSparseHoverTarget = function handleSparseHoverTarget(view, target, api) {
62
+ var _$pos$nodeAfter, _controls$activeNode, _anchors$get, _anchors$get2;
63
+ if (!api || target === view.dom) {
52
64
  return false;
53
65
  }
66
+ var controls = api.blockControls.sharedState.currentState();
54
67
  var position;
55
68
  try {
56
- position = view.posAtDOM(event.target, 0);
69
+ position = view.posAtDOM(target, 0);
57
70
  } catch (_unused) {
58
71
  return false;
59
72
  }
@@ -75,7 +88,7 @@ export var handleSparseMouseOver = function handleSparseMouseOver(view, event, a
75
88
  // Browser positions over non-editable node chrome can resolve to the first editable child.
76
89
  // Keep the ancestor unless that child actually owns the hovered DOM target.
77
90
  var nodeAfterDOM = view.nodeDOM(position);
78
- if (nodeAfterDOM instanceof Element && nodeAfterDOM.contains(event.target)) {
91
+ if (nodeAfterDOM instanceof Element && nodeAfterDOM.contains(target)) {
79
92
  targetPosition = position;
80
93
  }
81
94
  }
@@ -86,9 +99,9 @@ export var handleSparseMouseOver = function handleSparseMouseOver(view, event, a
86
99
  if (!initialNode) {
87
100
  return false;
88
101
  }
89
- var target = redirectParagraphToWrappedMedia(view, view.state.doc.resolve(targetPosition), targetPosition);
90
- targetPosition = target.position;
91
- var node = target.node;
102
+ var redirected = redirectParagraphToWrappedMedia(view, view.state.doc.resolve(targetPosition), targetPosition);
103
+ targetPosition = redirected.position;
104
+ var node = redirected.node;
92
105
  if (!node || (controls === null || controls === void 0 || (_controls$activeNode = controls.activeNode) === null || _controls$activeNode === void 0 ? void 0 : _controls$activeNode.pos) === targetPosition) {
93
106
  return false;
94
107
  }
@@ -35,7 +35,6 @@ import { getActiveDropTargetDecorations } from './decorations-drop-target-active
35
35
  import { createActiveQuickInsertNodeDecoration, findActiveQuickInsertNodeDec, findQuickInsertInsertButtonDecoration, quickInsertButtonDecoration, TYPE_ACTIVE_QUICK_INSERT_NODE } from './decorations-quick-insert-button';
36
36
  import { handleMouseDown } from './handle-mouse-down';
37
37
  import { handleMouseOver } from './handle-mouse-over';
38
- import { handleSparseMouseOver } from './handle-sparse-mouse-over';
39
38
  import { boundKeydownHandler } from './keymap';
40
39
  import { defaultActiveAnchorTracker } from './utils/active-anchor-tracker';
41
40
  import { getMultiSelectAnalyticsAttributes } from './utils/analytics';
@@ -1159,6 +1158,11 @@ export var createPlugin = function createPlugin(api, getIntl, nodeViewPortalProv
1159
1158
  },
1160
1159
  mouseover: function mouseover(view, event) {
1161
1160
  var _api$limitedMode7, _getNodeIdProvider;
1161
+ // Sparse surfaces handle all hover in bindSparseGutterHover, which also covers the gutters
1162
+ // outside view.dom.
1163
+ if (isExperimentEnabled('platform_editor_block_control_migration')) {
1164
+ return false;
1165
+ }
1162
1166
  if (api !== null && api !== void 0 && (_api$limitedMode7 = api.limitedMode) !== null && _api$limitedMode7 !== void 0 && (_api$limitedMode7 = _api$limitedMode7.sharedState.currentState()) !== null && _api$limitedMode7 !== void 0 && _api$limitedMode7.enabled) {
1163
1167
  return;
1164
1168
  }
@@ -1166,14 +1170,10 @@ export var createPlugin = function createPlugin(api, getIntl, nodeViewPortalProv
1166
1170
  // another layer of protection to prevent handling mouseover
1167
1171
  // when native anchor with dnd is enabled in limited mode
1168
1172
  // in case there are descripancies between getNodeIdProvider limited mode state
1169
- if ((_getNodeIdProvider = getNodeIdProvider(view)) !== null && _getNodeIdProvider !== void 0 && _getNodeIdProvider.isLimitedMode() && (expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) || isExperimentEnabled('platform_editor_block_control_migration'))) {
1173
+ if ((_getNodeIdProvider = getNodeIdProvider(view)) !== null && _getNodeIdProvider !== void 0 && _getNodeIdProvider.isLimitedMode() && expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) {
1170
1174
  return;
1171
1175
  }
1172
- if (isExperimentEnabled('platform_editor_block_control_migration')) {
1173
- handleSparseMouseOver(view, event, api);
1174
- } else {
1175
- handleMouseOver(view, event, api);
1176
- }
1176
+ handleMouseOver(view, event, api);
1177
1177
  return false;
1178
1178
  },
1179
1179
  mousedown: function mousedown(view, event) {