@atlaskit/editor-plugin-block-controls 7.7.5 → 7.7.7

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @atlaskit/editor-plugin-block-controls
2
2
 
3
+ ## 7.7.7
4
+
5
+ ### Patch Changes
6
+
7
+ - [`8854ad2383b33`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/8854ad2383b33) -
8
+ Suppress no-literal-string-in-jsx
9
+ - Updated dependencies
10
+
11
+ ## 7.7.6
12
+
13
+ ### Patch Changes
14
+
15
+ - [`253460bc61db3`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/253460bc61db3) -
16
+ [ux] EDITOR-3380 use preserved selection for block menu move action
17
+ - [`1b3603981c776`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/1b3603981c776) -
18
+ EDITOR-3380 ensure we preserve the same selection type
19
+ - Updated dependencies
20
+
3
21
  ## 7.7.5
4
22
 
5
23
  ### Patch Changes
@@ -21,6 +21,7 @@ var _firstNodeDecPlugin = require("./pm-plugins/first-node-dec-plugin");
21
21
  var _pmPlugin = require("./pm-plugins/interaction-tracking/pm-plugin");
22
22
  var _main = require("./pm-plugins/main");
23
23
  var _editorCommands = require("./pm-plugins/selection-preservation/editor-commands");
24
+ var _pluginKey = require("./pm-plugins/selection-preservation/plugin-key");
24
25
  var _pmPlugin2 = require("./pm-plugins/selection-preservation/pm-plugin");
25
26
  var _getSelection = require("./pm-plugins/utils/getSelection");
26
27
  var _blockMenu = _interopRequireDefault(require("./ui/block-menu"));
@@ -279,6 +280,10 @@ var blockControlsPlugin = exports.blockControlsPlugin = function blockControlsPl
279
280
  var _interactionTrackingP2, _interactionTrackingP3;
280
281
  sharedState.isMouseOut = (_interactionTrackingP2 = (_interactionTrackingP3 = _pmPlugin.interactionTrackingPluginKey.getState(editorState)) === null || _interactionTrackingP3 === void 0 ? void 0 : _interactionTrackingP3.isMouseOut) !== null && _interactionTrackingP2 !== void 0 ? _interactionTrackingP2 : false;
281
282
  }
283
+ if ((0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_block_menu', 'isEnabled', true)) {
284
+ var _selectionPreservatio;
285
+ sharedState.preservedSelection = (_selectionPreservatio = _pluginKey.selectionPreservationPluginKey.getState(editorState)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
286
+ }
282
287
  return sharedState;
283
288
  },
284
289
  contentComponent: function contentComponent(_ref8) {
@@ -12,14 +12,17 @@ var _moveNode = require("./move-node");
12
12
  var _moveNodeUtils = require("./utils/move-node-utils");
13
13
  var moveNodeWithBlockMenu = exports.moveNodeWithBlockMenu = function moveNodeWithBlockMenu(api, direction) {
14
14
  return function (_ref) {
15
+ var _api$blockControls$sh;
15
16
  var tr = _ref.tr;
16
17
  if (!(0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_block_menu', 'isEnabled', true)) {
17
18
  return tr;
18
19
  }
20
+ var preservedSelection = api === null || api === void 0 || (_api$blockControls$sh = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh === void 0 ? void 0 : _api$blockControls$sh.preservedSelection;
21
+ var selection = preservedSelection !== null && preservedSelection !== void 0 ? preservedSelection : tr.selection;
19
22
 
20
23
  // Nodes like lists nest within themselves, we need to find the top most position
21
24
  var currentNodePos = (0, _moveNodeUtils.getCurrentNodePosFromDragHandleSelection)({
22
- selection: tr.selection,
25
+ selection: selection,
23
26
  schema: tr.doc.type.schema,
24
27
  resolve: tr.doc.resolve.bind(tr.doc)
25
28
  });
@@ -15,13 +15,18 @@ var _utils = require("./utils");
15
15
  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; }
16
16
  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) { (0, _defineProperty2.default)(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; }
17
17
  /**
18
- * Selection Preservation Plugin for ProseMirror Editor
18
+ * Selection Preservation Plugin
19
19
  *
20
- * Solves a ProseMirror limitation where TextSelection cannot include positions at node boundaries
21
- * (like media/images). When a selection spans text + media nodes, subsequent transactions cause
22
- * ProseMirror to collapse the selection to the nearest inline position, excluding the media node.
23
- * This is problematic for features like block menus and drag-and-drop that need stable multi-node
24
- * selections while performing operations.
20
+ * Used to ensure the selection remains stable across selected nodes during specific UI operations,
21
+ * such as when block menus are open or during drag-and-drop actions.
22
+ *
23
+ * We use a TextSelection to span multi-node selections, however there is a ProseMirror limitation
24
+ * where TextSelection cannot include non inline positions at node boundaries (like media/images).
25
+ *
26
+ * When a selection spans text + media nodes, subsequent transactions cause ProseMirror to collapse
27
+ * the selection to the nearest inline position, excluding the media node. This is problematic for
28
+ * features like block menus and drag-and-drop that need stable multi-node selections while performing
29
+ * operations.
25
30
  *
26
31
  * The plugin works in three phases:
27
32
  * (1) Explicitly save a selection via startPreservingSelection() when opening block menus or starting drag operations.
@@ -50,23 +55,12 @@ var createSelectionPreservationPlugin = exports.createSelectionPreservationPlugi
50
55
  var meta = (0, _utils.getSelectionPreservationMeta)(tr);
51
56
  var newState = _objectSpread({}, pluginState);
52
57
  if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'startPreserving') {
53
- newState.preservedSelection = new _state.TextSelection(tr.selection.$from, tr.selection.$to);
58
+ newState.preservedSelection = tr.selection;
54
59
  } else if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'stopPreserving') {
55
60
  newState.preservedSelection = undefined;
56
61
  }
57
62
  if (newState.preservedSelection && tr.docChanged) {
58
- var from = tr.mapping.map(newState.preservedSelection.from);
59
- var to = tr.mapping.map(newState.preservedSelection.to);
60
- if (from < 0 || to > tr.doc.content.size || from >= to) {
61
- // stop preserving if preserved selection becomes invalid or collapsed to a cursor
62
- // e.g. after deleting the selection
63
- newState.preservedSelection = undefined;
64
- } else {
65
- var _expandToBlockRange = expandToBlockRange(tr.doc.resolve(from), tr.doc.resolve(to)),
66
- $from = _expandToBlockRange.$from,
67
- $to = _expandToBlockRange.$to;
68
- newState.preservedSelection = new _state.TextSelection($from, $to);
69
- }
63
+ newState.preservedSelection = mapSelection(newState.preservedSelection, tr);
70
64
  }
71
65
  return newState;
72
66
  }
@@ -91,7 +85,7 @@ var createSelectionPreservationPlugin = exports.createSelectionPreservationPlugi
91
85
  return null;
92
86
  }
93
87
  try {
94
- return newState.tr.setSelection(_state.TextSelection.create(newState.doc, savedSel.from, savedSel.to));
88
+ return newState.tr.setSelection(savedSel);
95
89
  } catch (error) {
96
90
  (0, _monitoring.logException)(error, {
97
91
  location: 'editor-plugin-block-controls/SelectionPreservationPlugin'
@@ -101,6 +95,30 @@ var createSelectionPreservationPlugin = exports.createSelectionPreservationPlugi
101
95
  }
102
96
  });
103
97
  };
98
+ var mapSelection = function mapSelection(selection, tr) {
99
+ if (selection instanceof _state.TextSelection) {
100
+ var from = tr.mapping.map(selection.from);
101
+ var to = tr.mapping.map(selection.to);
102
+
103
+ // expand the text selection range to block boundaries, so as document changes occur the
104
+ // selection always includes whole nodes
105
+ var _expandToBlockRange = expandToBlockRange(tr.doc.resolve(from), tr.doc.resolve(to)),
106
+ $from = _expandToBlockRange.$from,
107
+ $to = _expandToBlockRange.$to;
108
+
109
+ // stop preserving if preserved selection becomes invalid or collapsed to a cursor
110
+ // e.g. after deleting the selection
111
+ if ($from.pos < 0 || $to.pos > tr.doc.content.size || $from.pos >= $to.pos) {
112
+ return undefined;
113
+ }
114
+ return new _state.TextSelection($from, $to);
115
+ }
116
+ try {
117
+ return selection.map(tr.doc, tr.mapping);
118
+ } catch (_unused) {
119
+ return undefined;
120
+ }
121
+ };
104
122
  var expandToBlockRange = function expandToBlockRange($from, $to) {
105
123
  var range = $from.blockRange($to);
106
124
  if (!range) {
@@ -16,5 +16,6 @@ var DragHandleMenu = exports.DragHandleMenu = function DragHandleMenu(_ref) {
16
16
  };
17
17
  }),
18
18
  isMenuOpen = _useSharedPluginState.isMenuOpen;
19
+ // eslint-disable-next-line @atlassian/i18n/no-literal-string-in-jsx
19
20
  return isMenuOpen ? /*#__PURE__*/_react.default.createElement("div", null, "menu") : null;
20
21
  };
@@ -13,6 +13,7 @@ import { firstNodeDecPlugin } from './pm-plugins/first-node-dec-plugin';
13
13
  import { createInteractionTrackingPlugin, interactionTrackingPluginKey } from './pm-plugins/interaction-tracking/pm-plugin';
14
14
  import { createPlugin, key } from './pm-plugins/main';
15
15
  import { startPreservingSelection, stopPreservingSelection } from './pm-plugins/selection-preservation/editor-commands';
16
+ import { selectionPreservationPluginKey } from './pm-plugins/selection-preservation/plugin-key';
16
17
  import { createSelectionPreservationPlugin } from './pm-plugins/selection-preservation/pm-plugin';
17
18
  import { selectNode } from './pm-plugins/utils/getSelection';
18
19
  import BlockMenu from './ui/block-menu';
@@ -269,6 +270,10 @@ export const blockControlsPlugin = ({
269
270
  var _interactionTrackingP2, _interactionTrackingP3;
270
271
  sharedState.isMouseOut = (_interactionTrackingP2 = (_interactionTrackingP3 = interactionTrackingPluginKey.getState(editorState)) === null || _interactionTrackingP3 === void 0 ? void 0 : _interactionTrackingP3.isMouseOut) !== null && _interactionTrackingP2 !== void 0 ? _interactionTrackingP2 : false;
271
272
  }
273
+ if (expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true)) {
274
+ var _selectionPreservatio;
275
+ sharedState.preservedSelection = (_selectionPreservatio = selectionPreservationPluginKey.getState(editorState)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
276
+ }
272
277
  return sharedState;
273
278
  },
274
279
  contentComponent({
@@ -8,13 +8,16 @@ export const moveNodeWithBlockMenu = (api, direction) => {
8
8
  return ({
9
9
  tr
10
10
  }) => {
11
+ var _api$blockControls$sh;
11
12
  if (!expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true)) {
12
13
  return tr;
13
14
  }
15
+ const preservedSelection = 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.preservedSelection;
16
+ const selection = preservedSelection !== null && preservedSelection !== void 0 ? preservedSelection : tr.selection;
14
17
 
15
18
  // Nodes like lists nest within themselves, we need to find the top most position
16
19
  const currentNodePos = getCurrentNodePosFromDragHandleSelection({
17
- selection: tr.selection,
20
+ selection,
18
21
  schema: tr.doc.type.schema,
19
22
  resolve: tr.doc.resolve.bind(tr.doc)
20
23
  });
@@ -6,13 +6,18 @@ import { selectionPreservationPluginKey } from './plugin-key';
6
6
  import { getSelectionPreservationMeta, hasUserSelectionChange } from './utils';
7
7
 
8
8
  /**
9
- * Selection Preservation Plugin for ProseMirror Editor
9
+ * Selection Preservation Plugin
10
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.
11
+ * Used to ensure the selection remains stable across selected nodes during specific UI operations,
12
+ * such as when block menus are open or during drag-and-drop actions.
13
+ *
14
+ * We use a TextSelection to span multi-node selections, however there is a ProseMirror limitation
15
+ * where TextSelection cannot include non inline positions at node boundaries (like media/images).
16
+ *
17
+ * When a selection spans text + media nodes, subsequent transactions cause ProseMirror to collapse
18
+ * the selection to the nearest inline position, excluding the media node. This is problematic for
19
+ * features like block menus and drag-and-drop that need stable multi-node selections while performing
20
+ * operations.
16
21
  *
17
22
  * The plugin works in three phases:
18
23
  * (1) Explicitly save a selection via startPreservingSelection() when opening block menus or starting drag operations.
@@ -43,24 +48,12 @@ export const createSelectionPreservationPlugin = () => {
43
48
  ...pluginState
44
49
  };
45
50
  if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'startPreserving') {
46
- newState.preservedSelection = new TextSelection(tr.selection.$from, tr.selection.$to);
51
+ newState.preservedSelection = tr.selection;
47
52
  } else if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'stopPreserving') {
48
53
  newState.preservedSelection = undefined;
49
54
  }
50
55
  if (newState.preservedSelection && tr.docChanged) {
51
- const from = tr.mapping.map(newState.preservedSelection.from);
52
- const to = tr.mapping.map(newState.preservedSelection.to);
53
- if (from < 0 || to > tr.doc.content.size || from >= to) {
54
- // stop preserving if preserved selection becomes invalid or collapsed to a cursor
55
- // e.g. after deleting the selection
56
- newState.preservedSelection = undefined;
57
- } else {
58
- const {
59
- $from,
60
- $to
61
- } = expandToBlockRange(tr.doc.resolve(from), tr.doc.resolve(to));
62
- newState.preservedSelection = new TextSelection($from, $to);
63
- }
56
+ newState.preservedSelection = mapSelection(newState.preservedSelection, tr);
64
57
  }
65
58
  return newState;
66
59
  }
@@ -85,7 +78,7 @@ export const createSelectionPreservationPlugin = () => {
85
78
  return null;
86
79
  }
87
80
  try {
88
- return newState.tr.setSelection(TextSelection.create(newState.doc, savedSel.from, savedSel.to));
81
+ return newState.tr.setSelection(savedSel);
89
82
  } catch (error) {
90
83
  logException(error, {
91
84
  location: 'editor-plugin-block-controls/SelectionPreservationPlugin'
@@ -95,6 +88,31 @@ export const createSelectionPreservationPlugin = () => {
95
88
  }
96
89
  });
97
90
  };
91
+ const mapSelection = (selection, tr) => {
92
+ if (selection instanceof TextSelection) {
93
+ const from = tr.mapping.map(selection.from);
94
+ const to = tr.mapping.map(selection.to);
95
+
96
+ // expand the text selection range to block boundaries, so as document changes occur the
97
+ // selection always includes whole nodes
98
+ const {
99
+ $from,
100
+ $to
101
+ } = expandToBlockRange(tr.doc.resolve(from), tr.doc.resolve(to));
102
+
103
+ // stop preserving if preserved selection becomes invalid or collapsed to a cursor
104
+ // e.g. after deleting the selection
105
+ if ($from.pos < 0 || $to.pos > tr.doc.content.size || $from.pos >= $to.pos) {
106
+ return undefined;
107
+ }
108
+ return new TextSelection($from, $to);
109
+ }
110
+ try {
111
+ return selection.map(tr.doc, tr.mapping);
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ };
98
116
  const expandToBlockRange = ($from, $to) => {
99
117
  const range = $from.blockRange($to);
100
118
  if (!range) {
@@ -11,5 +11,6 @@ export const DragHandleMenu = ({
11
11
  isMenuOpen: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.isMenuOpen
12
12
  };
13
13
  });
14
+ // eslint-disable-next-line @atlassian/i18n/no-literal-string-in-jsx
14
15
  return isMenuOpen ? /*#__PURE__*/React.createElement("div", null, "menu") : null;
15
16
  };
@@ -16,6 +16,7 @@ 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
18
  import { startPreservingSelection as _startPreservingSelection, stopPreservingSelection as _stopPreservingSelection } from './pm-plugins/selection-preservation/editor-commands';
19
+ import { selectionPreservationPluginKey } from './pm-plugins/selection-preservation/plugin-key';
19
20
  import { createSelectionPreservationPlugin } from './pm-plugins/selection-preservation/pm-plugin';
20
21
  import { selectNode } from './pm-plugins/utils/getSelection';
21
22
  import BlockMenu from './ui/block-menu';
@@ -272,6 +273,10 @@ export var blockControlsPlugin = function blockControlsPlugin(_ref) {
272
273
  var _interactionTrackingP2, _interactionTrackingP3;
273
274
  sharedState.isMouseOut = (_interactionTrackingP2 = (_interactionTrackingP3 = interactionTrackingPluginKey.getState(editorState)) === null || _interactionTrackingP3 === void 0 ? void 0 : _interactionTrackingP3.isMouseOut) !== null && _interactionTrackingP2 !== void 0 ? _interactionTrackingP2 : false;
274
275
  }
276
+ if (expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true)) {
277
+ var _selectionPreservatio;
278
+ sharedState.preservedSelection = (_selectionPreservatio = selectionPreservationPluginKey.getState(editorState)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
279
+ }
275
280
  return sharedState;
276
281
  },
277
282
  contentComponent: function contentComponent(_ref8) {
@@ -6,14 +6,17 @@ import { moveNode } from './move-node';
6
6
  import { getCurrentNodePosFromDragHandleSelection, getPosWhenMoveNodeDown, getPosWhenMoveNodeUp, getShouldMoveNode } from './utils/move-node-utils';
7
7
  export var moveNodeWithBlockMenu = function moveNodeWithBlockMenu(api, direction) {
8
8
  return function (_ref) {
9
+ var _api$blockControls$sh;
9
10
  var tr = _ref.tr;
10
11
  if (!expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true)) {
11
12
  return tr;
12
13
  }
14
+ var preservedSelection = api === null || api === void 0 || (_api$blockControls$sh = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh === void 0 ? void 0 : _api$blockControls$sh.preservedSelection;
15
+ var selection = preservedSelection !== null && preservedSelection !== void 0 ? preservedSelection : tr.selection;
13
16
 
14
17
  // Nodes like lists nest within themselves, we need to find the top most position
15
18
  var currentNodePos = getCurrentNodePosFromDragHandleSelection({
16
- selection: tr.selection,
19
+ selection: selection,
17
20
  schema: tr.doc.type.schema,
18
21
  resolve: tr.doc.resolve.bind(tr.doc)
19
22
  });
@@ -9,13 +9,18 @@ import { selectionPreservationPluginKey } from './plugin-key';
9
9
  import { getSelectionPreservationMeta, hasUserSelectionChange } from './utils';
10
10
 
11
11
  /**
12
- * Selection Preservation Plugin for ProseMirror Editor
12
+ * Selection Preservation Plugin
13
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.
14
+ * Used to ensure the selection remains stable across selected nodes during specific UI operations,
15
+ * such as when block menus are open or during drag-and-drop actions.
16
+ *
17
+ * We use a TextSelection to span multi-node selections, however there is a ProseMirror limitation
18
+ * where TextSelection cannot include non inline positions at node boundaries (like media/images).
19
+ *
20
+ * When a selection spans text + media nodes, subsequent transactions cause ProseMirror to collapse
21
+ * the selection to the nearest inline position, excluding the media node. This is problematic for
22
+ * features like block menus and drag-and-drop that need stable multi-node selections while performing
23
+ * operations.
19
24
  *
20
25
  * The plugin works in three phases:
21
26
  * (1) Explicitly save a selection via startPreservingSelection() when opening block menus or starting drag operations.
@@ -44,23 +49,12 @@ export var createSelectionPreservationPlugin = function createSelectionPreservat
44
49
  var meta = getSelectionPreservationMeta(tr);
45
50
  var newState = _objectSpread({}, pluginState);
46
51
  if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'startPreserving') {
47
- newState.preservedSelection = new TextSelection(tr.selection.$from, tr.selection.$to);
52
+ newState.preservedSelection = tr.selection;
48
53
  } else if ((meta === null || meta === void 0 ? void 0 : meta.type) === 'stopPreserving') {
49
54
  newState.preservedSelection = undefined;
50
55
  }
51
56
  if (newState.preservedSelection && tr.docChanged) {
52
- var from = tr.mapping.map(newState.preservedSelection.from);
53
- var to = tr.mapping.map(newState.preservedSelection.to);
54
- if (from < 0 || to > tr.doc.content.size || from >= to) {
55
- // stop preserving if preserved selection becomes invalid or collapsed to a cursor
56
- // e.g. after deleting the selection
57
- newState.preservedSelection = undefined;
58
- } else {
59
- var _expandToBlockRange = expandToBlockRange(tr.doc.resolve(from), tr.doc.resolve(to)),
60
- $from = _expandToBlockRange.$from,
61
- $to = _expandToBlockRange.$to;
62
- newState.preservedSelection = new TextSelection($from, $to);
63
- }
57
+ newState.preservedSelection = mapSelection(newState.preservedSelection, tr);
64
58
  }
65
59
  return newState;
66
60
  }
@@ -85,7 +79,7 @@ export var createSelectionPreservationPlugin = function createSelectionPreservat
85
79
  return null;
86
80
  }
87
81
  try {
88
- return newState.tr.setSelection(TextSelection.create(newState.doc, savedSel.from, savedSel.to));
82
+ return newState.tr.setSelection(savedSel);
89
83
  } catch (error) {
90
84
  logException(error, {
91
85
  location: 'editor-plugin-block-controls/SelectionPreservationPlugin'
@@ -95,6 +89,30 @@ export var createSelectionPreservationPlugin = function createSelectionPreservat
95
89
  }
96
90
  });
97
91
  };
92
+ var mapSelection = function mapSelection(selection, tr) {
93
+ if (selection instanceof TextSelection) {
94
+ var from = tr.mapping.map(selection.from);
95
+ var to = tr.mapping.map(selection.to);
96
+
97
+ // expand the text selection range to block boundaries, so as document changes occur the
98
+ // selection always includes whole nodes
99
+ var _expandToBlockRange = expandToBlockRange(tr.doc.resolve(from), tr.doc.resolve(to)),
100
+ $from = _expandToBlockRange.$from,
101
+ $to = _expandToBlockRange.$to;
102
+
103
+ // stop preserving if preserved selection becomes invalid or collapsed to a cursor
104
+ // e.g. after deleting the selection
105
+ if ($from.pos < 0 || $to.pos > tr.doc.content.size || $from.pos >= $to.pos) {
106
+ return undefined;
107
+ }
108
+ return new TextSelection($from, $to);
109
+ }
110
+ try {
111
+ return selection.map(tr.doc, tr.mapping);
112
+ } catch (_unused) {
113
+ return undefined;
114
+ }
115
+ };
98
116
  var expandToBlockRange = function expandToBlockRange($from, $to) {
99
117
  var range = $from.blockRange($to);
100
118
  if (!range) {
@@ -9,5 +9,6 @@ export var DragHandleMenu = function DragHandleMenu(_ref) {
9
9
  };
10
10
  }),
11
11
  isMenuOpen = _useSharedPluginState.isMenuOpen;
12
+ // eslint-disable-next-line @atlassian/i18n/no-literal-string-in-jsx
12
13
  return isMenuOpen ? /*#__PURE__*/React.createElement("div", null, "menu") : null;
13
14
  };
@@ -14,6 +14,7 @@ import type { ToolbarPlugin } from '@atlaskit/editor-plugin-toolbar';
14
14
  import type { TypeAheadPlugin } from '@atlaskit/editor-plugin-type-ahead';
15
15
  import type { UserIntentPlugin } from '@atlaskit/editor-plugin-user-intent';
16
16
  import type { WidthPlugin } from '@atlaskit/editor-plugin-width';
17
+ import type { Selection } from '@atlaskit/editor-prosemirror/state';
17
18
  import { type DecorationSet } from '@atlaskit/editor-prosemirror/view';
18
19
  export type ActiveNode = {
19
20
  anchorName: string;
@@ -71,6 +72,7 @@ export interface PluginState {
71
72
  menuTriggerBy?: string;
72
73
  menuTriggerByNode?: TriggerByNode;
73
74
  multiSelectDnD?: MultiSelectDnD;
75
+ preservedSelection?: Selection;
74
76
  }
75
77
  export type ReleaseHiddenDecoration = () => boolean | undefined;
76
78
  export type BlockControlsSharedState = {
@@ -92,6 +94,7 @@ export type BlockControlsSharedState = {
92
94
  menuTriggerBy?: string;
93
95
  menuTriggerByNode?: TriggerByNode;
94
96
  multiSelectDnD?: MultiSelectDnD;
97
+ preservedSelection?: Selection;
95
98
  } | undefined;
96
99
  export type HandleOptions = {
97
100
  isFocused: boolean;
@@ -1,13 +1,18 @@
1
1
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
2
2
  import type { SelectionPreservationPluginState } from './types';
3
3
  /**
4
- * Selection Preservation Plugin for ProseMirror Editor
4
+ * Selection Preservation Plugin
5
5
  *
6
- * Solves a ProseMirror limitation where TextSelection cannot include positions at node boundaries
7
- * (like media/images). When a selection spans text + media nodes, subsequent transactions cause
8
- * ProseMirror to collapse the selection to the nearest inline position, excluding the media node.
9
- * This is problematic for features like block menus and drag-and-drop that need stable multi-node
10
- * selections while performing operations.
6
+ * Used to ensure the selection remains stable across selected nodes during specific UI operations,
7
+ * such as when block menus are open or during drag-and-drop actions.
8
+ *
9
+ * We use a TextSelection to span multi-node selections, however there is a ProseMirror limitation
10
+ * where TextSelection cannot include non inline positions at node boundaries (like media/images).
11
+ *
12
+ * When a selection spans text + media nodes, subsequent transactions cause ProseMirror to collapse
13
+ * the selection to the nearest inline position, excluding the media node. This is problematic for
14
+ * features like block menus and drag-and-drop that need stable multi-node selections while performing
15
+ * operations.
11
16
  *
12
17
  * The plugin works in three phases:
13
18
  * (1) Explicitly save a selection via startPreservingSelection() when opening block menus or starting drag operations.
@@ -14,6 +14,7 @@ import type { ToolbarPlugin } from '@atlaskit/editor-plugin-toolbar';
14
14
  import type { TypeAheadPlugin } from '@atlaskit/editor-plugin-type-ahead';
15
15
  import type { UserIntentPlugin } from '@atlaskit/editor-plugin-user-intent';
16
16
  import type { WidthPlugin } from '@atlaskit/editor-plugin-width';
17
+ import type { Selection } from '@atlaskit/editor-prosemirror/state';
17
18
  import { type DecorationSet } from '@atlaskit/editor-prosemirror/view';
18
19
  export type ActiveNode = {
19
20
  anchorName: string;
@@ -71,6 +72,7 @@ export interface PluginState {
71
72
  menuTriggerBy?: string;
72
73
  menuTriggerByNode?: TriggerByNode;
73
74
  multiSelectDnD?: MultiSelectDnD;
75
+ preservedSelection?: Selection;
74
76
  }
75
77
  export type ReleaseHiddenDecoration = () => boolean | undefined;
76
78
  export type BlockControlsSharedState = {
@@ -92,6 +94,7 @@ export type BlockControlsSharedState = {
92
94
  menuTriggerBy?: string;
93
95
  menuTriggerByNode?: TriggerByNode;
94
96
  multiSelectDnD?: MultiSelectDnD;
97
+ preservedSelection?: Selection;
95
98
  } | undefined;
96
99
  export type HandleOptions = {
97
100
  isFocused: boolean;
@@ -1,13 +1,18 @@
1
1
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
2
2
  import type { SelectionPreservationPluginState } from './types';
3
3
  /**
4
- * Selection Preservation Plugin for ProseMirror Editor
4
+ * Selection Preservation Plugin
5
5
  *
6
- * Solves a ProseMirror limitation where TextSelection cannot include positions at node boundaries
7
- * (like media/images). When a selection spans text + media nodes, subsequent transactions cause
8
- * ProseMirror to collapse the selection to the nearest inline position, excluding the media node.
9
- * This is problematic for features like block menus and drag-and-drop that need stable multi-node
10
- * selections while performing operations.
6
+ * Used to ensure the selection remains stable across selected nodes during specific UI operations,
7
+ * such as when block menus are open or during drag-and-drop actions.
8
+ *
9
+ * We use a TextSelection to span multi-node selections, however there is a ProseMirror limitation
10
+ * where TextSelection cannot include non inline positions at node boundaries (like media/images).
11
+ *
12
+ * When a selection spans text + media nodes, subsequent transactions cause ProseMirror to collapse
13
+ * the selection to the nearest inline position, excluding the media node. This is problematic for
14
+ * features like block menus and drag-and-drop that need stable multi-node selections while performing
15
+ * operations.
11
16
  *
12
17
  * The plugin works in three phases:
13
18
  * (1) Explicitly save a selection via startPreservingSelection() when opening block menus or starting drag operations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-block-controls",
3
- "version": "7.7.5",
3
+ "version": "7.7.7",
4
4
  "description": "Block controls plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -54,8 +54,8 @@
54
54
  "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^3.2.0",
55
55
  "@atlaskit/primitives": "^16.2.0",
56
56
  "@atlaskit/theme": "^21.0.0",
57
- "@atlaskit/tmp-editor-statsig": "^13.40.0",
58
- "@atlaskit/tokens": "^8.1.0",
57
+ "@atlaskit/tmp-editor-statsig": "^13.41.0",
58
+ "@atlaskit/tokens": "^8.2.0",
59
59
  "@atlaskit/tooltip": "^20.10.0",
60
60
  "@babel/runtime": "^7.0.0",
61
61
  "@emotion/react": "^11.7.1",