@atlaskit/editor-plugin-block-controls 7.7.2 → 7.7.3

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 (35) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cjs/blockControlsPlugin.js +14 -0
  3. package/dist/cjs/pm-plugins/selection-preservation/editor-commands.js +32 -0
  4. package/dist/cjs/pm-plugins/selection-preservation/plugin-key.js +8 -0
  5. package/dist/cjs/pm-plugins/selection-preservation/pm-plugin.js +99 -0
  6. package/dist/cjs/pm-plugins/selection-preservation/types.js +5 -0
  7. package/dist/cjs/pm-plugins/selection-preservation/utils.js +24 -0
  8. package/dist/cjs/ui/drag-handle.js +182 -62
  9. package/dist/es2019/blockControlsPlugin.js +11 -1
  10. package/dist/es2019/pm-plugins/selection-preservation/editor-commands.js +28 -0
  11. package/dist/es2019/pm-plugins/selection-preservation/plugin-key.js +2 -0
  12. package/dist/es2019/pm-plugins/selection-preservation/pm-plugin.js +92 -0
  13. package/dist/es2019/pm-plugins/selection-preservation/types.js +1 -0
  14. package/dist/es2019/pm-plugins/selection-preservation/utils.js +16 -0
  15. package/dist/es2019/ui/drag-handle.js +156 -33
  16. package/dist/esm/blockControlsPlugin.js +14 -0
  17. package/dist/esm/pm-plugins/selection-preservation/editor-commands.js +26 -0
  18. package/dist/esm/pm-plugins/selection-preservation/plugin-key.js +2 -0
  19. package/dist/esm/pm-plugins/selection-preservation/pm-plugin.js +93 -0
  20. package/dist/esm/pm-plugins/selection-preservation/types.js +1 -0
  21. package/dist/esm/pm-plugins/selection-preservation/utils.js +18 -0
  22. package/dist/esm/ui/drag-handle.js +184 -64
  23. package/dist/types/blockControlsPluginType.d.ts +12 -1
  24. package/dist/types/pm-plugins/selection-preservation/editor-commands.d.ts +13 -0
  25. package/dist/types/pm-plugins/selection-preservation/plugin-key.d.ts +3 -0
  26. package/dist/types/pm-plugins/selection-preservation/pm-plugin.d.ts +26 -0
  27. package/dist/types/pm-plugins/selection-preservation/types.d.ts +7 -0
  28. package/dist/types/pm-plugins/selection-preservation/utils.d.ts +10 -0
  29. package/dist/types-ts4.5/blockControlsPluginType.d.ts +12 -1
  30. package/dist/types-ts4.5/pm-plugins/selection-preservation/editor-commands.d.ts +13 -0
  31. package/dist/types-ts4.5/pm-plugins/selection-preservation/plugin-key.d.ts +3 -0
  32. package/dist/types-ts4.5/pm-plugins/selection-preservation/pm-plugin.d.ts +26 -0
  33. package/dist/types-ts4.5/pm-plugins/selection-preservation/types.d.ts +7 -0
  34. package/dist/types-ts4.5/pm-plugins/selection-preservation/utils.d.ts +10 -0
  35. package/package.json +4 -3
@@ -0,0 +1,28 @@
1
+ import { selectionPreservationPluginKey } from './plugin-key';
2
+ /**
3
+ * Start preserving the selection when a UI interaction requires it
4
+ *
5
+ * e.g., block menu open, drag-and-drop in progress
6
+ */
7
+ export const startPreservingSelection = ({
8
+ tr
9
+ }) => {
10
+ const meta = {
11
+ type: 'startPreserving'
12
+ };
13
+ return tr.setMeta(selectionPreservationPluginKey, meta);
14
+ };
15
+
16
+ /**
17
+ * Stop preserving the selection when a UI interaction completes
18
+ *
19
+ * e.g., block menu closed, drag-and-drop ended
20
+ */
21
+ export const stopPreservingSelection = ({
22
+ tr
23
+ }) => {
24
+ const meta = {
25
+ type: 'stopPreserving'
26
+ };
27
+ return tr.setMeta(selectionPreservationPluginKey, meta);
28
+ };
@@ -0,0 +1,2 @@
1
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
2
+ export const selectionPreservationPluginKey = new PluginKey('selectionPreservationPlugin');
@@ -0,0 +1,92 @@
1
+ import { logException } from '@atlaskit/editor-common/monitoring';
2
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
3
+ import { TextSelection } from '@atlaskit/editor-prosemirror/state';
4
+ import { stopPreservingSelection } from './editor-commands';
5
+ import { selectionPreservationPluginKey } from './plugin-key';
6
+ import { getSelectionPreservationMeta, hasUserSelectionChange } from './utils';
7
+
8
+ /**
9
+ * Selection Preservation Plugin for ProseMirror Editor
10
+ *
11
+ * Solves a ProseMirror limitation where TextSelection cannot include positions at node boundaries
12
+ * (like media/images). When a selection spans text + media nodes, subsequent transactions cause
13
+ * ProseMirror to collapse the selection to the nearest inline position, excluding the media node.
14
+ * This is problematic for features like block menus and drag-and-drop that need stable multi-node
15
+ * selections while performing operations.
16
+ *
17
+ * The plugin works in three phases:
18
+ * (1) Explicitly save a selection via startPreservingSelection() when opening block menus or starting drag operations.
19
+ * (2) Map the saved selection through document changes to keep positions valid.
20
+ * (3) Detect when transactions collapse the selection and restore it via appendTransaction().
21
+ *
22
+ * Stops preserving via stopPreservingSelection() when the menu closes or operation completes.
23
+ *
24
+ * Commands: startPreservingSelection() to begin preservation, stopPreservingSelection() to end it.
25
+ *
26
+ * NOTE: Only use when the UI blocks user selection changes. For example: when a block menu overlay
27
+ * is open (editor becomes non-interactive), during drag-and-drop operations (user is mid-drag), or
28
+ * when modal dialogs are active. In these states, any selection changes are from ProseMirror's
29
+ * internal behavior (not user input) and should be prevented. Do not use during normal editing.
30
+ */
31
+ export const createSelectionPreservationPlugin = () => {
32
+ return new SafePlugin({
33
+ key: selectionPreservationPluginKey,
34
+ state: {
35
+ init() {
36
+ return {
37
+ preservedSelection: undefined
38
+ };
39
+ },
40
+ apply(tr, pluginState) {
41
+ const meta = getSelectionPreservationMeta(tr);
42
+ const newState = {
43
+ ...pluginState
44
+ };
45
+ if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'startPreserving') {
46
+ newState.preservedSelection = new TextSelection(tr.selection.$from, tr.selection.$to);
47
+ } else if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'stopPreserving') {
48
+ newState.preservedSelection = undefined;
49
+ }
50
+ if (newState.preservedSelection && tr.docChanged) {
51
+ const mapped = new TextSelection(newState.preservedSelection.$from, newState.preservedSelection.$to);
52
+ mapped.map(tr.doc, tr.mapping);
53
+ if (mapped.from >= 0 && mapped.to <= tr.doc.content.size && mapped.from !== mapped.to) {
54
+ newState.preservedSelection = mapped;
55
+ } else if (mapped.from === mapped.to) {
56
+ // If selection has collapsed to a cursor, e.g. after deleting the selection, stop preserving
57
+ newState.preservedSelection = undefined;
58
+ }
59
+ }
60
+ return newState;
61
+ }
62
+ },
63
+ appendTransaction(transactions, _oldState, newState) {
64
+ const pluginState = selectionPreservationPluginKey.getState(newState);
65
+ const savedSel = pluginState === null || pluginState === void 0 ? void 0 : pluginState.preservedSelection;
66
+ if (!savedSel) {
67
+ return null;
68
+ }
69
+ if (hasUserSelectionChange(transactions)) {
70
+ // Auto-stop if user explicitly changes selection
71
+ return stopPreservingSelection({
72
+ tr: newState.tr
73
+ });
74
+ }
75
+ const currSel = newState.selection;
76
+ const wasEmptySelection = savedSel.from === savedSel.to;
77
+ const selectionUnchanged = currSel.from === savedSel.from && currSel.to === savedSel.to;
78
+ const selectionInvalid = savedSel.from < 0 || savedSel.to > newState.doc.content.size;
79
+ if (wasEmptySelection || selectionUnchanged || selectionInvalid) {
80
+ return null;
81
+ }
82
+ try {
83
+ return newState.tr.setSelection(TextSelection.create(newState.doc, savedSel.from, savedSel.to));
84
+ } catch (error) {
85
+ logException(error, {
86
+ location: 'editor-plugin-block-controls/SelectionPreservationPlugin'
87
+ });
88
+ }
89
+ return null;
90
+ }
91
+ });
92
+ };
@@ -0,0 +1,16 @@
1
+ import { selectionPreservationPluginKey } from './plugin-key';
2
+ /**
3
+ * Detects if any of the transactions include user-driven selection changes.
4
+ *
5
+ * @param transactions The list of transactions to check.
6
+ * @returns True if any transaction includes a user-driven selection change, otherwise false.
7
+ */
8
+ export const hasUserSelectionChange = transactions => {
9
+ return transactions.some(tr => tr.getMeta('pointer') || tr.getMeta('uiEvent') || tr.getMeta('paste') || tr.getMeta('cut') || tr.getMeta('composition') ||
10
+ // IME input
11
+ // Keyboard events that change selection
12
+ tr.getMeta('addToHistory') && tr.selectionSet);
13
+ };
14
+ export const getSelectionPreservationMeta = tr => {
15
+ return tr.getMeta(selectionPreservationPluginKey);
16
+ };
@@ -8,17 +8,18 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
8
  // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
9
9
  import { css, jsx } from '@emotion/react';
10
10
  import { bind } from 'bind-event-listener';
11
+ import { getDocument } from '@atlaskit/browser-apis';
11
12
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
12
13
  import { browser as browserLegacy, getBrowserInfo } from '@atlaskit/editor-common/browser';
13
14
  import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
14
15
  import { dragToMoveDown, dragToMoveLeft, dragToMoveRight, dragToMoveUp, getAriaKeyshortcuts, TooltipContentWithMultipleShortcuts } from '@atlaskit/editor-common/keymaps';
15
16
  import { blockControlsMessages } from '@atlaskit/editor-common/messages';
16
17
  import { deleteSelectedRange } from '@atlaskit/editor-common/selection';
17
- import { tableControlsSpacing, DRAG_HANDLE_WIDTH } from '@atlaskit/editor-common/styles';
18
+ import { DRAG_HANDLE_WIDTH, tableControlsSpacing } from '@atlaskit/editor-common/styles';
18
19
  import { useSharedPluginStateSelector } from '@atlaskit/editor-common/use-shared-plugin-state-selector';
19
20
  import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
20
21
  import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
21
- import { akEditorTableToolbarSize, akEditorFullPageNarrowBreakout, relativeSizeToBaseFontSize } from '@atlaskit/editor-shared-styles/consts';
22
+ import { akEditorFullPageNarrowBreakout, akEditorTableToolbarSize, relativeSizeToBaseFontSize } from '@atlaskit/editor-shared-styles/consts';
22
23
  import DragHandleVerticalIcon from '@atlaskit/icon/core/drag-handle-vertical';
23
24
  import DragHandlerIcon from '@atlaskit/icon/glyph/drag-handler';
24
25
  import { fg } from '@atlaskit/platform-feature-flags';
@@ -32,6 +33,7 @@ import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
32
33
  import Tooltip from '@atlaskit/tooltip';
33
34
  import { getNodeTypeWithLevel } from '../pm-plugins/decorations-common';
34
35
  import { key } from '../pm-plugins/main';
36
+ import { selectionPreservationPluginKey } from '../pm-plugins/selection-preservation/plugin-key';
35
37
  import { getMultiSelectAnalyticsAttributes } from '../pm-plugins/utils/analytics';
36
38
  import { getControlBottomCSSValue, getControlHeightCSSValue, getLeftPosition, getNodeHeight, getTopPosition, shouldBeSticky, shouldMaskNodeControls } from '../pm-plugins/utils/drag-handle-positions';
37
39
  import { isHandleCorrelatedToSelection, isNodeWithCodeBlock, selectNode } from '../pm-plugins/utils/getSelection';
@@ -291,6 +293,71 @@ const getNodeMargins = node => {
291
293
  }
292
294
  return nodeMargins[nodeTypeName] || nodeMargins['default'];
293
295
  };
296
+ const isRangeSpanningMultipleNodes = range => {
297
+ if (range.endIndex - range.startIndex <= 1) {
298
+ return false; // At most one child
299
+ }
300
+
301
+ // Count block nodes in the range, return true if more than one
302
+ let blockCount = 0;
303
+ for (let i = range.startIndex; i < range.endIndex; i++) {
304
+ if (range.parent.child(i).isBlock) {
305
+ blockCount++;
306
+ }
307
+ if (blockCount > 1) {
308
+ return true;
309
+ }
310
+ }
311
+ return false;
312
+ };
313
+ const shouldExpandSelection = (range, startPos) => {
314
+ return !!range && isRangeSpanningMultipleNodes(range) && range.start <= startPos && range.end >= startPos + 1;
315
+ };
316
+ const calculateBlockRange = ({
317
+ selection,
318
+ doc,
319
+ resolvedStartPos,
320
+ isShiftPressed
321
+ }) => {
322
+ if (!isShiftPressed) {
323
+ // When not pressing shift, create range including all block nodes within the selection
324
+ return selection.$from.blockRange(selection.$to);
325
+ }
326
+ if (resolvedStartPos.pos < selection.from) {
327
+ // If shift+click selecting upwards, get range from start of node to end of selection
328
+ return resolvedStartPos.blockRange(selection.$to);
329
+ }
330
+
331
+ // Shift+click selecting downwards, get range from start of selection to pos within or after node
332
+ const resolvedPosWithinOrAfterNode = doc.resolve(resolvedStartPos.pos + 1);
333
+ return selection.$from.blockRange(resolvedPosWithinOrAfterNode);
334
+ };
335
+ const createExpandedSelection = (doc, selection, range) => {
336
+ return TextSelection.create(doc, Math.min(selection.from, range.start), Math.max(selection.to, range.end));
337
+ };
338
+ const createSelectionFromRange = ({
339
+ tr,
340
+ selection,
341
+ startPos,
342
+ nodeType,
343
+ range,
344
+ api
345
+ }) => {
346
+ if (range && shouldExpandSelection(range, startPos)) {
347
+ const expandedSelection = createExpandedSelection(tr.doc, selection, range);
348
+ if (!expandedSelection.eq(tr.selection)) {
349
+ tr.setSelection(expandedSelection);
350
+ }
351
+ return tr;
352
+ }
353
+ const node = tr.doc.nodeAt(startPos);
354
+ const isEmptyNode = (node === null || node === void 0 ? void 0 : node.content.size) === 0;
355
+ if (isEmptyNode && node.type.name !== 'paragraph') {
356
+ tr.setSelection(new NodeSelection(tr.doc.resolve(startPos)));
357
+ return tr;
358
+ }
359
+ return selectNode(tr, startPos, nodeType, api);
360
+ };
294
361
  export const DragHandle = ({
295
362
  view,
296
363
  api,
@@ -302,7 +369,7 @@ export const DragHandle = ({
302
369
  isTopLevelNode = true,
303
370
  anchorRectCache
304
371
  }) => {
305
- var _api$core3;
372
+ var _api$core4;
306
373
  const buttonRef = useRef(null);
307
374
  const [dragHandleSelected, setDragHandleSelected] = useState(false);
308
375
  const [dragHandleDisabled, setDragHandleDisabled] = useState(false);
@@ -344,15 +411,71 @@ export const DragHandle = ({
344
411
  }
345
412
  }
346
413
  }, [anchorName, nodeType, view.dom]);
347
- const handleOnClick = useCallback(e => {
414
+ const handleOnClickNew = useCallback(e => {
348
415
  var _api$core;
416
+ api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
417
+ tr
418
+ }) => {
419
+ var _api$analytics, _resolvedStartPos$nod, _selectionPreservatio, _api$blockControls, _api$blockControls2;
420
+ const startPos = getPos();
421
+ if (startPos === undefined) {
422
+ return tr;
423
+ }
424
+ const resolvedStartPos = tr.doc.resolve(startPos);
425
+ api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.attachAnalyticsEvent({
426
+ eventType: EVENT_TYPE.UI,
427
+ action: ACTION.CLICKED,
428
+ actionSubject: ACTION_SUBJECT.BUTTON,
429
+ actionSubjectId: ACTION_SUBJECT_ID.ELEMENT_DRAG_HANDLE,
430
+ attributes: {
431
+ nodeDepth: resolvedStartPos.depth,
432
+ nodeType: ((_resolvedStartPos$nod = resolvedStartPos.nodeAfter) === null || _resolvedStartPos$nod === void 0 ? void 0 : _resolvedStartPos$nod.type.name) || ''
433
+ }
434
+ })(tr);
435
+ const preservedSelection = (_selectionPreservatio = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
436
+ const selection = preservedSelection || tr.selection;
437
+ const range = calculateBlockRange({
438
+ doc: tr.doc,
439
+ selection,
440
+ resolvedStartPos,
441
+ isShiftPressed: e.shiftKey
442
+ });
443
+ tr = createSelectionFromRange({
444
+ tr,
445
+ selection,
446
+ startPos,
447
+ nodeType,
448
+ range,
449
+ api
450
+ });
451
+ api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.commands.startPreservingSelection()({
452
+ tr
453
+ });
454
+ api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : _api$blockControls2.commands.toggleBlockMenu({
455
+ anchorName,
456
+ openedViaKeyboard: false,
457
+ triggerByNode: expValEqualsNoExposure('platform_synced_block', 'isEnabled', true) ? {
458
+ nodeType,
459
+ pos: startPos,
460
+ rootPos: tr.doc.resolve(startPos).before(1)
461
+ } : undefined
462
+ })({
463
+ tr
464
+ });
465
+ tr.setMeta('scrollIntoView', false);
466
+ return tr;
467
+ });
468
+ view.focus();
469
+ }, [api, view, getPos, nodeType, anchorName]);
470
+ const handleOnClick = useCallback(e => {
471
+ var _api$core2;
349
472
  if (!isMultiSelect) {
350
473
  setDragHandleSelected(!dragHandleSelected);
351
474
  }
352
- api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
475
+ api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
353
476
  tr
354
477
  }) => {
355
- var _api$blockControls$sh, _api$analytics;
478
+ var _api$blockControls$sh, _api$analytics2;
356
479
  const startPos = getPos();
357
480
  if (startPos === undefined) {
358
481
  return tr;
@@ -381,8 +504,8 @@ export const DragHandle = ({
381
504
  rootPos
382
505
  } : undefined;
383
506
  if (BLOCK_MENU_ENABLED && editorExperiment('platform_editor_controls', 'variant1')) {
384
- var _api$blockControls;
385
- api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.commands.toggleBlockMenu({
507
+ var _api$blockControls3;
508
+ api === null || api === void 0 ? void 0 : (_api$blockControls3 = api.blockControls) === null || _api$blockControls3 === void 0 ? void 0 : _api$blockControls3.commands.toggleBlockMenu({
386
509
  anchorName,
387
510
  triggerByNode,
388
511
  openedViaKeyboard: expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? false : undefined
@@ -391,8 +514,8 @@ export const DragHandle = ({
391
514
  });
392
515
  e.stopPropagation();
393
516
  } else if (expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true)) {
394
- var _api$blockControls2;
395
- api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : _api$blockControls2.commands.toggleBlockMenu({
517
+ var _api$blockControls4;
518
+ api === null || api === void 0 ? void 0 : (_api$blockControls4 = api.blockControls) === null || _api$blockControls4 === void 0 ? void 0 : _api$blockControls4.commands.toggleBlockMenu({
396
519
  anchorName,
397
520
  triggerByNode,
398
521
  openedViaKeyboard: expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? false : undefined
@@ -402,18 +525,18 @@ export const DragHandle = ({
402
525
  e.stopPropagation();
403
526
  }
404
527
  } else if (isTopLevelNode && $anchor.depth <= DRAG_HANDLE_MAX_SHIFT_CLICK_DEPTH && e.shiftKey && fg('platform_editor_elements_dnd_shift_click_select')) {
405
- var _api$blockControls3;
528
+ var _api$blockControls5;
406
529
  const alignAnchorHeadToSel = alignAnchorHeadInDirectionOfPos(tr.selection, startPos);
407
530
  const selectionWithExpandedHead = expandSelectionHeadToNodeAtPos(alignAnchorHeadToSel, startPos);
408
531
  tr.setSelection(selectionWithExpandedHead);
409
- api === null || api === void 0 ? void 0 : (_api$blockControls3 = api.blockControls) === null || _api$blockControls3 === void 0 ? void 0 : _api$blockControls3.commands.setMultiSelectPositions()({
532
+ api === null || api === void 0 ? void 0 : (_api$blockControls5 = api.blockControls) === null || _api$blockControls5 === void 0 ? void 0 : _api$blockControls5.commands.setMultiSelectPositions()({
410
533
  tr
411
534
  });
412
535
  }
413
536
  const resolvedMovingNode = tr.doc.resolve(startPos);
414
537
  const maybeNode = resolvedMovingNode.nodeAfter;
415
538
  tr.setMeta('scrollIntoView', false);
416
- api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.attachAnalyticsEvent({
539
+ api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions.attachAnalyticsEvent({
417
540
  eventType: EVENT_TYPE.UI,
418
541
  action: ACTION.CLICKED,
419
542
  actionSubject: ACTION_SUBJECT.BUTTON,
@@ -430,9 +553,9 @@ export const DragHandle = ({
430
553
  const handleKeyDown = useCallback(e => {
431
554
  // allow user to use spacebar to select the node
432
555
  if (!e.repeat && e.key === ' ') {
433
- var _api$core2;
556
+ var _api$core3;
434
557
  const startPos = getPos();
435
- api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
558
+ api === null || api === void 0 ? void 0 : (_api$core3 = api.core) === null || _api$core3 === void 0 ? void 0 : _api$core3.actions.execute(({
436
559
  tr
437
560
  }) => {
438
561
  if (startPos === undefined) {
@@ -455,21 +578,21 @@ export const DragHandle = ({
455
578
  // return focus to editor to resume editing from caret position
456
579
  view.focus();
457
580
  }
458
- }, [getPos, api === null || api === void 0 ? void 0 : (_api$core3 = api.core) === null || _api$core3 === void 0 ? void 0 : _api$core3.actions, isMultiSelect, view]);
581
+ }, [getPos, api === null || api === void 0 ? void 0 : (_api$core4 = api.core) === null || _api$core4 === void 0 ? void 0 : _api$core4.actions, isMultiSelect, view]);
459
582
  const handleKeyDownNew = useCallback(e => {
460
583
  // allow user to use spacebar to select the node
461
584
  if (e.key === 'Enter' || !e.repeat && e.key === ' ') {
462
- var _api$core4;
463
- if (document.activeElement !== buttonRef.current) {
585
+ var _getDocument, _api$core5;
586
+ if (((_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement) !== buttonRef.current) {
464
587
  return;
465
588
  }
466
589
  e.preventDefault();
467
590
  e.stopPropagation();
468
591
  const startPos = getPos();
469
- api === null || api === void 0 ? void 0 : (_api$core4 = api.core) === null || _api$core4 === void 0 ? void 0 : _api$core4.actions.execute(({
592
+ api === null || api === void 0 ? void 0 : (_api$core5 = api.core) === null || _api$core5 === void 0 ? void 0 : _api$core5.actions.execute(({
470
593
  tr
471
594
  }) => {
472
- var _api$blockControls4, _api$userIntent;
595
+ var _api$blockControls6, _api$userIntent;
473
596
  if (startPos === undefined) {
474
597
  return tr;
475
598
  }
@@ -483,7 +606,7 @@ export const DragHandle = ({
483
606
  pos: startPos,
484
607
  rootPos
485
608
  } : undefined;
486
- api === null || api === void 0 ? void 0 : (_api$blockControls4 = api.blockControls) === null || _api$blockControls4 === void 0 ? void 0 : _api$blockControls4.commands.toggleBlockMenu({
609
+ api === null || api === void 0 ? void 0 : (_api$blockControls6 = api.blockControls) === null || _api$blockControls6 === void 0 ? void 0 : _api$blockControls6.commands.toggleBlockMenu({
487
610
  anchorName,
488
611
  triggerByNode,
489
612
  openedViaKeyboard: true
@@ -501,9 +624,9 @@ export const DragHandle = ({
501
624
  api === null || api === void 0 ? void 0 : api.core.actions.execute(({
502
625
  tr
503
626
  }) => {
504
- var _api$blockControls5;
627
+ var _api$blockControls7;
505
628
  deleteSelectedRange(tr);
506
- api === null || api === void 0 ? void 0 : (_api$blockControls5 = api.blockControls) === null || _api$blockControls5 === void 0 ? void 0 : _api$blockControls5.commands.toggleBlockMenu({
629
+ api === null || api === void 0 ? void 0 : (_api$blockControls7 = api.blockControls) === null || _api$blockControls7 === void 0 ? void 0 : _api$blockControls7.commands.toggleBlockMenu({
507
630
  closeMenu: true
508
631
  })({
509
632
  tr
@@ -532,8 +655,8 @@ export const DragHandle = ({
532
655
  }) => {
533
656
  var _api$blockControls$sh2;
534
657
  if (isMultiSelect) {
535
- var _api$core5;
536
- api === null || api === void 0 ? void 0 : (_api$core5 = api.core) === null || _api$core5 === void 0 ? void 0 : _api$core5.actions.execute(({
658
+ var _api$core6;
659
+ api === null || api === void 0 ? void 0 : (_api$core6 = api.core) === null || _api$core6 === void 0 ? void 0 : _api$core6.actions.execute(({
537
660
  tr
538
661
  }) => {
539
662
  const handlePos = getPos();
@@ -542,8 +665,8 @@ export const DragHandle = ({
542
665
  }
543
666
  const newHandlePosCheck = isHandleCorrelatedToSelection(view.state, tr.selection, handlePos);
544
667
  if (!tr.selection.empty && newHandlePosCheck) {
545
- var _api$blockControls6;
546
- api === null || api === void 0 ? void 0 : (_api$blockControls6 = api.blockControls) === null || _api$blockControls6 === void 0 ? void 0 : _api$blockControls6.commands.setMultiSelectPositions()({
668
+ var _api$blockControls8;
669
+ api === null || api === void 0 ? void 0 : (_api$blockControls8 = api.blockControls) === null || _api$blockControls8 === void 0 ? void 0 : _api$blockControls8.commands.setMultiSelectPositions()({
547
670
  tr
548
671
  });
549
672
  } else if (fg('platform_editor_elements_dnd_select_node_on_drag')) {
@@ -651,14 +774,14 @@ export const DragHandle = ({
651
774
  });
652
775
  },
653
776
  onDragStart() {
654
- var _api$core6;
777
+ var _api$core7;
655
778
  if (start === undefined) {
656
779
  return;
657
780
  }
658
- api === null || api === void 0 ? void 0 : (_api$core6 = api.core) === null || _api$core6 === void 0 ? void 0 : _api$core6.actions.execute(({
781
+ api === null || api === void 0 ? void 0 : (_api$core7 = api.core) === null || _api$core7 === void 0 ? void 0 : _api$core7.actions.execute(({
659
782
  tr
660
783
  }) => {
661
- var _api$blockControls$sh3, _api$blockControls7, _api$analytics2;
784
+ var _api$blockControls$sh3, _api$blockControls9, _api$analytics3;
662
785
  let nodeTypes, hasSelectedMultipleNodes;
663
786
  const resolvedMovingNode = tr.doc.resolve(start);
664
787
  const maybeNode = resolvedMovingNode.nodeAfter;
@@ -671,11 +794,11 @@ export const DragHandle = ({
671
794
  nodeTypes = maybeNode === null || maybeNode === void 0 ? void 0 : maybeNode.type.name;
672
795
  hasSelectedMultipleNodes = false;
673
796
  }
674
- api === null || api === void 0 ? void 0 : (_api$blockControls7 = api.blockControls) === null || _api$blockControls7 === void 0 ? void 0 : _api$blockControls7.commands.setNodeDragged(getPos, anchorName, nodeType)({
797
+ api === null || api === void 0 ? void 0 : (_api$blockControls9 = api.blockControls) === null || _api$blockControls9 === void 0 ? void 0 : _api$blockControls9.commands.setNodeDragged(getPos, anchorName, nodeType)({
675
798
  tr
676
799
  });
677
800
  tr.setMeta('scrollIntoView', false);
678
- api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions.attachAnalyticsEvent({
801
+ api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions.attachAnalyticsEvent({
679
802
  eventType: EVENT_TYPE.UI,
680
803
  action: ACTION.DRAGGED,
681
804
  actionSubject: ACTION_SUBJECT.ELEMENT,
@@ -973,7 +1096,7 @@ export const DragHandle = ({
973
1096
  // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
974
1097
  ,
975
1098
  style: !editorExperiment('platform_editor_controls', 'variant1') ? editorExperiment('platform_editor_block_control_optimise_render', true) ? positionStyles : positionStylesOld : {},
976
- onClick: handleOnClick,
1099
+ onClick: expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true) ? handleOnClickNew : handleOnClick,
977
1100
  onKeyDown: expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true) && expValEqualsNoExposure('platform_editor_block_menu_keyboard_navigation', 'isEnabled', true) ? handleKeyDownNew : handleKeyDown
978
1101
  // eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop
979
1102
  ,
@@ -15,6 +15,8 @@ import { canMoveNodeUpOrDown } from './editor-commands/utils/move-node-utils';
15
15
  import { firstNodeDecPlugin } from './pm-plugins/first-node-dec-plugin';
16
16
  import { createInteractionTrackingPlugin, interactionTrackingPluginKey } from './pm-plugins/interaction-tracking/pm-plugin';
17
17
  import { createPlugin, key } from './pm-plugins/main';
18
+ import { startPreservingSelection as _startPreservingSelection, stopPreservingSelection as _stopPreservingSelection } from './pm-plugins/selection-preservation/editor-commands';
19
+ import { createSelectionPreservationPlugin } from './pm-plugins/selection-preservation/pm-plugin';
18
20
  import { selectNode } from './pm-plugins/utils/getSelection';
19
21
  import BlockMenu from './ui/block-menu';
20
22
  import { DragHandleMenu } from './ui/drag-handle-menu';
@@ -38,6 +40,12 @@ export var blockControlsPlugin = function blockControlsPlugin(_ref) {
38
40
  plugin: createInteractionTrackingPlugin
39
41
  });
40
42
  }
43
+ if (expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true)) {
44
+ pmPlugins.push({
45
+ name: 'blockControlsSelectionPreservationPlugin',
46
+ plugin: createSelectionPreservationPlugin
47
+ });
48
+ }
41
49
 
42
50
  // platform_editor_controls note: quick insert rendering fixes
43
51
  if (areToolbarFlagsEnabled(Boolean(api === null || api === void 0 ? void 0 : api.toolbar))) {
@@ -232,6 +240,12 @@ export var blockControlsPlugin = function blockControlsPlugin(_ref) {
232
240
  },
233
241
  moveNodeWithBlockMenu: function moveNodeWithBlockMenu(direction) {
234
242
  return _moveNodeWithBlockMenu(api, direction);
243
+ },
244
+ startPreservingSelection: function startPreservingSelection() {
245
+ return _startPreservingSelection;
246
+ },
247
+ stopPreservingSelection: function stopPreservingSelection() {
248
+ return _stopPreservingSelection;
235
249
  }
236
250
  },
237
251
  getSharedState: function getSharedState(editorState) {
@@ -0,0 +1,26 @@
1
+ import { selectionPreservationPluginKey } from './plugin-key';
2
+ /**
3
+ * Start preserving the selection when a UI interaction requires it
4
+ *
5
+ * e.g., block menu open, drag-and-drop in progress
6
+ */
7
+ export var startPreservingSelection = function startPreservingSelection(_ref) {
8
+ var tr = _ref.tr;
9
+ var meta = {
10
+ type: 'startPreserving'
11
+ };
12
+ return tr.setMeta(selectionPreservationPluginKey, meta);
13
+ };
14
+
15
+ /**
16
+ * Stop preserving the selection when a UI interaction completes
17
+ *
18
+ * e.g., block menu closed, drag-and-drop ended
19
+ */
20
+ export var stopPreservingSelection = function stopPreservingSelection(_ref2) {
21
+ var tr = _ref2.tr;
22
+ var meta = {
23
+ type: 'stopPreserving'
24
+ };
25
+ return tr.setMeta(selectionPreservationPluginKey, meta);
26
+ };
@@ -0,0 +1,2 @@
1
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
2
+ export var selectionPreservationPluginKey = new PluginKey('selectionPreservationPlugin');
@@ -0,0 +1,93 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
+ import { logException } from '@atlaskit/editor-common/monitoring';
5
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
6
+ import { TextSelection } from '@atlaskit/editor-prosemirror/state';
7
+ import { stopPreservingSelection } from './editor-commands';
8
+ import { selectionPreservationPluginKey } from './plugin-key';
9
+ import { getSelectionPreservationMeta, hasUserSelectionChange } from './utils';
10
+
11
+ /**
12
+ * Selection Preservation Plugin for ProseMirror Editor
13
+ *
14
+ * Solves a ProseMirror limitation where TextSelection cannot include positions at node boundaries
15
+ * (like media/images). When a selection spans text + media nodes, subsequent transactions cause
16
+ * ProseMirror to collapse the selection to the nearest inline position, excluding the media node.
17
+ * This is problematic for features like block menus and drag-and-drop that need stable multi-node
18
+ * selections while performing operations.
19
+ *
20
+ * The plugin works in three phases:
21
+ * (1) Explicitly save a selection via startPreservingSelection() when opening block menus or starting drag operations.
22
+ * (2) Map the saved selection through document changes to keep positions valid.
23
+ * (3) Detect when transactions collapse the selection and restore it via appendTransaction().
24
+ *
25
+ * Stops preserving via stopPreservingSelection() when the menu closes or operation completes.
26
+ *
27
+ * Commands: startPreservingSelection() to begin preservation, stopPreservingSelection() to end it.
28
+ *
29
+ * NOTE: Only use when the UI blocks user selection changes. For example: when a block menu overlay
30
+ * is open (editor becomes non-interactive), during drag-and-drop operations (user is mid-drag), or
31
+ * when modal dialogs are active. In these states, any selection changes are from ProseMirror's
32
+ * internal behavior (not user input) and should be prevented. Do not use during normal editing.
33
+ */
34
+ export var createSelectionPreservationPlugin = function createSelectionPreservationPlugin() {
35
+ return new SafePlugin({
36
+ key: selectionPreservationPluginKey,
37
+ state: {
38
+ init: function init() {
39
+ return {
40
+ preservedSelection: undefined
41
+ };
42
+ },
43
+ apply: function apply(tr, pluginState) {
44
+ var meta = getSelectionPreservationMeta(tr);
45
+ var newState = _objectSpread({}, pluginState);
46
+ if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'startPreserving') {
47
+ newState.preservedSelection = new TextSelection(tr.selection.$from, tr.selection.$to);
48
+ } else if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'stopPreserving') {
49
+ newState.preservedSelection = undefined;
50
+ }
51
+ if (newState.preservedSelection && tr.docChanged) {
52
+ var mapped = new TextSelection(newState.preservedSelection.$from, newState.preservedSelection.$to);
53
+ mapped.map(tr.doc, tr.mapping);
54
+ if (mapped.from >= 0 && mapped.to <= tr.doc.content.size && mapped.from !== mapped.to) {
55
+ newState.preservedSelection = mapped;
56
+ } else if (mapped.from === mapped.to) {
57
+ // If selection has collapsed to a cursor, e.g. after deleting the selection, stop preserving
58
+ newState.preservedSelection = undefined;
59
+ }
60
+ }
61
+ return newState;
62
+ }
63
+ },
64
+ appendTransaction: function appendTransaction(transactions, _oldState, newState) {
65
+ var pluginState = selectionPreservationPluginKey.getState(newState);
66
+ var savedSel = pluginState === null || pluginState === void 0 ? void 0 : pluginState.preservedSelection;
67
+ if (!savedSel) {
68
+ return null;
69
+ }
70
+ if (hasUserSelectionChange(transactions)) {
71
+ // Auto-stop if user explicitly changes selection
72
+ return stopPreservingSelection({
73
+ tr: newState.tr
74
+ });
75
+ }
76
+ var currSel = newState.selection;
77
+ var wasEmptySelection = savedSel.from === savedSel.to;
78
+ var selectionUnchanged = currSel.from === savedSel.from && currSel.to === savedSel.to;
79
+ var selectionInvalid = savedSel.from < 0 || savedSel.to > newState.doc.content.size;
80
+ if (wasEmptySelection || selectionUnchanged || selectionInvalid) {
81
+ return null;
82
+ }
83
+ try {
84
+ return newState.tr.setSelection(TextSelection.create(newState.doc, savedSel.from, savedSel.to));
85
+ } catch (error) {
86
+ logException(error, {
87
+ location: 'editor-plugin-block-controls/SelectionPreservationPlugin'
88
+ });
89
+ }
90
+ return null;
91
+ }
92
+ });
93
+ };