@atlaskit/editor-plugin-block-controls 8.2.1 → 8.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @atlaskit/editor-plugin-block-controls
2
2
 
3
+ ## 8.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`9d67968b1ac03`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/9d67968b1ac03) -
8
+ [ux] Fixed issue with browser selection sync logic for preserved selection plugin
9
+ - Updated dependencies
10
+
11
+ ## 8.3.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [`617747c789f4e`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/617747c789f4e) -
16
+ Use correct editorExperiment instead of expVal for evaluating platform_synced_block
17
+
3
18
  ## 8.2.1
4
19
 
5
20
  ### Patch Changes
@@ -46,7 +46,7 @@ var shouldCollapseMargin = function shouldCollapseMargin(prevNode, nextNode) {
46
46
  return true;
47
47
  };
48
48
  var getGapAndOffset = function getGapAndOffset(prevNode, nextNode, parentNode) {
49
- var isSyncBlockOffsetPatchEnabled = (0, _expValEquals.expValEquals)('platform_synced_block', 'isEnabled', true) && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_2');
49
+ var isSyncBlockOffsetPatchEnabled = (0, _experiments.editorExperiment)('platform_synced_block', true) && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_2');
50
50
  if (!prevNode && nextNode) {
51
51
  // first node - adjust for bodied containers
52
52
  var _offset = 0;
@@ -256,7 +256,7 @@ var dropTargetDecorations = exports.dropTargetDecorations = function dropTargetD
256
256
  return false; //not valid pos, so nested not valid either
257
257
  }
258
258
  }
259
- var parentTypesWithEndDropTarget = (0, _expValEquals.expValEquals)('platform_synced_block', 'isEnabled', true) && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_2') ? PARENT_WITH_END_DROP_TARGET_NEXT : PARENT_WITH_END_DROP_TARGET;
259
+ var parentTypesWithEndDropTarget = (0, _experiments.editorExperiment)('platform_synced_block', true) && (0, _platformFeatureFlags.fg)('platform_synced_block_patch_2') ? PARENT_WITH_END_DROP_TARGET_NEXT : PARENT_WITH_END_DROP_TARGET;
260
260
  if (parent.lastChild === node && !(0, _utils.isEmptyParagraph)(node) && parentTypesWithEndDropTarget.includes(parent.type.name)) {
261
261
  endPos = pos + node.nodeSize;
262
262
  }
@@ -7,8 +7,10 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.createSelectionPreservationPlugin = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
9
  var _bindEventListener = require("bind-event-listener");
10
+ var _browserApis = require("@atlaskit/browser-apis");
10
11
  var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
11
12
  var _styles = require("@atlaskit/editor-common/styles");
13
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
12
14
  var _main = require("../main");
13
15
  var _selection = require("../utils/selection");
14
16
  var _editorCommands = require("./editor-commands");
@@ -109,7 +111,14 @@ var createSelectionPreservationPlugin = exports.createSelectionPreservationPlugi
109
111
  },
110
112
  view: function view(initialView) {
111
113
  var view = initialView;
112
- var unbindDocumentMouseDown = (0, _bindEventListener.bind)(document, {
114
+ var doc = (0, _browserApis.getDocument)();
115
+ if (!doc) {
116
+ return {
117
+ update: function update() {},
118
+ destroy: function destroy() {}
119
+ };
120
+ }
121
+ var unbindDocumentMouseDown = (0, _bindEventListener.bind)(doc, {
113
122
  type: 'mousedown',
114
123
  listener: function listener(e) {
115
124
  if (!(e.target instanceof HTMLElement)) {
@@ -150,14 +159,42 @@ var createSelectionPreservationPlugin = exports.createSelectionPreservationPlugi
150
159
  });
151
160
  return {
152
161
  update: function update(updateView, prevState) {
153
- var _selectionPreservatio, _selectionPreservatio2, _key$getState, _key$getState2;
154
162
  view = updateView;
155
- var prevPreservedSelection = (_selectionPreservatio = _pluginKey.selectionPreservationPluginKey.getState(prevState)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
156
- var currPreservedSelection = (_selectionPreservatio2 = _pluginKey.selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio2 === void 0 ? void 0 : _selectionPreservatio2.preservedSelection;
157
- var prevActiveNode = (_key$getState = _main.key.getState(prevState)) === null || _key$getState === void 0 ? void 0 : _key$getState.activeNode;
158
- var currActiveNode = (_key$getState2 = _main.key.getState(view.state)) === null || _key$getState2 === void 0 ? void 0 : _key$getState2.activeNode;
159
- if (currPreservedSelection && view.hasFocus() && (!(0, _utils.compareSelections)(prevPreservedSelection, currPreservedSelection) || prevActiveNode !== currActiveNode)) {
160
- (0, _utils.syncDOMSelection)(view.state.selection);
163
+
164
+ // [FEATURE FLAG: platform_editor_selection_sync_fix]
165
+ // When enabled, syncs DOM selection even when editor doesn't have focus.
166
+ // This prevents ghost highlighting after moving nodes when block menu is open.
167
+ // To clean up: remove the if-else block and keep only the flag-on behavior.
168
+ if ((0, _platformFeatureFlags.fg)('platform_editor_selection_sync_fix')) {
169
+ var _selectionPreservatio, _selectionPreservatio2, _key$getState, _key$getState2;
170
+ var prevPreservedSelection = (_selectionPreservatio = _pluginKey.selectionPreservationPluginKey.getState(prevState)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
171
+ var currPreservedSelection = (_selectionPreservatio2 = _pluginKey.selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio2 === void 0 ? void 0 : _selectionPreservatio2.preservedSelection;
172
+ var prevActiveNode = (_key$getState = _main.key.getState(prevState)) === null || _key$getState === void 0 ? void 0 : _key$getState.activeNode;
173
+ var currActiveNode = (_key$getState2 = _main.key.getState(view.state)) === null || _key$getState2 === void 0 ? void 0 : _key$getState2.activeNode;
174
+
175
+ // Sync DOM selection when the preserved selection or active node changes
176
+ // AND the document has changed (e.g., nodes moved)
177
+ // This prevents stealing focus during menu navigation while still fixing ghost highlighting
178
+ var hasPreservedSelection = !!currPreservedSelection;
179
+ var preservedSelectionChanged = !(0, _utils.compareSelections)(prevPreservedSelection, currPreservedSelection);
180
+ var activeNodeChanged = prevActiveNode !== currActiveNode;
181
+ var docChanged = prevState.doc !== view.state.doc;
182
+ var shouldSyncDOMSelection = hasPreservedSelection && (preservedSelectionChanged || activeNodeChanged) && docChanged;
183
+ if (shouldSyncDOMSelection) {
184
+ (0, _utils.syncDOMSelection)(view.state.selection, view);
185
+ }
186
+ } else {
187
+ var _selectionPreservatio3, _selectionPreservatio4, _key$getState3, _key$getState4;
188
+ // OLD BEHAVIOR (to be removed when flag is cleaned up)
189
+ // Only synced when editor had focus, causing ghost highlighting issues
190
+ var _prevPreservedSelection = (_selectionPreservatio3 = _pluginKey.selectionPreservationPluginKey.getState(prevState)) === null || _selectionPreservatio3 === void 0 ? void 0 : _selectionPreservatio3.preservedSelection;
191
+ var _currPreservedSelection = (_selectionPreservatio4 = _pluginKey.selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio4 === void 0 ? void 0 : _selectionPreservatio4.preservedSelection;
192
+ var _prevActiveNode = (_key$getState3 = _main.key.getState(prevState)) === null || _key$getState3 === void 0 ? void 0 : _key$getState3.activeNode;
193
+ var _currActiveNode = (_key$getState4 = _main.key.getState(view.state)) === null || _key$getState4 === void 0 ? void 0 : _key$getState4.activeNode;
194
+ if (_currPreservedSelection && view.hasFocus() && (!(0, _utils.compareSelections)(_prevPreservedSelection, _currPreservedSelection) || _prevActiveNode !== _currActiveNode)) {
195
+ // Old syncDOMSelection signature (to be removed)
196
+ (0, _utils.syncDOMSelection)(view.state.selection);
197
+ }
161
198
  }
162
199
  },
163
200
  destroy: function destroy() {
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.syncDOMSelection = exports.hasUserSelectionChange = exports.getSelectionPreservationMeta = exports.compareSelections = void 0;
7
+ var _browserApis = require("@atlaskit/browser-apis");
8
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
7
9
  var _pluginKey = require("./plugin-key");
8
10
  /**
9
11
  * Detects if any of the transactions include user-driven selection changes.
@@ -32,23 +34,61 @@ var compareSelections = exports.compareSelections = function compareSelections(a
32
34
  };
33
35
 
34
36
  /**
35
- * Triggers a DOM selection sync by resetting the current native selection range
36
- * only if it is out of sync with the provided ProseMirror selection state.
37
+ * Forces the browser's native selection to match ProseMirror's selection state.
37
38
  *
38
- * This is a necessary workaround to ensure the browser's native selection state
39
- * stays in sync with the preserved selection, particularly after transactions
40
- * that shift document content.
39
+ * This is necessary when the editor doesn't have focus (e.g., when block menu is open)
40
+ * but we still need to update the visual selection after moving nodes. Without this,
41
+ * the browser's native selection remains at the old position, causing ghost highlighting.
41
42
  *
42
- * @param selection The current ProseMirror selection state to compare against.
43
+ * @param selection The current ProseMirror selection state to sync to DOM.
44
+ * @param view The EditorView instance used to convert ProseMirror positions to DOM positions (when feature flag is enabled).
43
45
  */
44
- var syncDOMSelection = exports.syncDOMSelection = function syncDOMSelection(selection) {
45
- var domSelection = window.getSelection();
46
- var domRange = domSelection && domSelection.rangeCount === 1 && domSelection.getRangeAt(0).cloneRange();
47
- var isOutOfSync = domRange && (selection.from !== domRange.startOffset || selection.to !== domRange.endOffset);
48
- if (isOutOfSync) {
49
- // Force the DOM selection to refresh, setting it to the same range
50
- // This will trigger ProseMirror to re-apply its selection logic based on the current state
51
- domSelection.removeAllRanges();
52
- domSelection.addRange(domRange);
46
+ var syncDOMSelection = exports.syncDOMSelection = function syncDOMSelection(selection, view) {
47
+ // [FEATURE FLAG: platform_editor_selection_sync_fix]
48
+ // When enabled, uses improved DOM selection syncing with EditorView.
49
+ // To clean up: remove the if-else block, remove the optional view parameter,
50
+ // make view required, and keep only the flag-on behavior.
51
+ if (view && (0, _platformFeatureFlags.fg)('platform_editor_selection_sync_fix')) {
52
+ try {
53
+ var domSelection = window.getSelection();
54
+ if (!domSelection) {
55
+ return;
56
+ }
57
+ var doc = (0, _browserApis.getDocument)();
58
+ if (!doc) {
59
+ return;
60
+ }
61
+
62
+ // Convert ProseMirror selection to DOM selection using view.domAtPos
63
+ var anchor = view.domAtPos(selection.anchor);
64
+ var head = view.domAtPos(selection.head);
65
+ if (!anchor || !head) {
66
+ return;
67
+ }
68
+
69
+ // Create a new DOM range from the ProseMirror selection
70
+ var range = doc.createRange();
71
+ range.setStart(anchor.node, anchor.offset);
72
+ range.setEnd(head.node, head.offset);
73
+
74
+ // Update the DOM selection to match ProseMirror's selection
75
+ domSelection.removeAllRanges();
76
+ domSelection.addRange(range);
77
+ } catch (_unused) {
78
+ // Silently fail if DOM selection sync fails
79
+ // This can happen if positions are invalid or DOM hasn't updated yet
80
+ }
81
+ } else {
82
+ // OLD BEHAVIOR (to be removed when flag is cleaned up)
83
+ // Only checked if selection was out of sync using incorrect offset comparison
84
+ var _domSelection = window.getSelection();
85
+ var domRange = _domSelection && _domSelection.rangeCount === 1 && _domSelection.getRangeAt(0).cloneRange();
86
+ var isOutOfSync = domRange && (selection.from !== domRange.startOffset || selection.to !== domRange.endOffset);
87
+ if (isOutOfSync && _domSelection && domRange) {
88
+ // Force the DOM selection to refresh, setting it to the same range
89
+ // This will trigger ProseMirror to re-apply its selection logic based on the current state
90
+ _domSelection.removeAllRanges();
91
+ _domSelection.addRange(domRange);
92
+ }
53
93
  }
54
94
  };
@@ -37,7 +37,7 @@ const shouldCollapseMargin = (prevNode, nextNode) => {
37
37
  return true;
38
38
  };
39
39
  const getGapAndOffset = (prevNode, nextNode, parentNode) => {
40
- const isSyncBlockOffsetPatchEnabled = expValEquals('platform_synced_block', 'isEnabled', true) && fg('platform_synced_block_patch_2');
40
+ const isSyncBlockOffsetPatchEnabled = editorExperiment('platform_synced_block', true) && fg('platform_synced_block_patch_2');
41
41
  if (!prevNode && nextNode) {
42
42
  // first node - adjust for bodied containers
43
43
  let offset = 0;
@@ -245,7 +245,7 @@ export const dropTargetDecorations = (newState, api, formatMessage, nodeViewPort
245
245
  return false; //not valid pos, so nested not valid either
246
246
  }
247
247
  }
248
- const parentTypesWithEndDropTarget = expValEquals('platform_synced_block', 'isEnabled', true) && fg('platform_synced_block_patch_2') ? PARENT_WITH_END_DROP_TARGET_NEXT : PARENT_WITH_END_DROP_TARGET;
248
+ const parentTypesWithEndDropTarget = editorExperiment('platform_synced_block', true) && fg('platform_synced_block_patch_2') ? PARENT_WITH_END_DROP_TARGET_NEXT : PARENT_WITH_END_DROP_TARGET;
249
249
  if (parent.lastChild === node && !isEmptyParagraph(node) && parentTypesWithEndDropTarget.includes(parent.type.name)) {
250
250
  endPos = pos + node.nodeSize;
251
251
  }
@@ -1,6 +1,8 @@
1
1
  import { bind } from 'bind-event-listener';
2
+ import { getDocument } from '@atlaskit/browser-apis';
2
3
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
3
4
  import { DRAG_HANDLE_SELECTOR } from '@atlaskit/editor-common/styles';
5
+ import { fg } from '@atlaskit/platform-feature-flags';
4
6
  import { key } from '../main';
5
7
  import { createPreservedSelection, mapPreservedSelection } from '../utils/selection';
6
8
  import { stopPreservingSelection } from './editor-commands';
@@ -101,7 +103,14 @@ export const createSelectionPreservationPlugin = api => () => {
101
103
  },
102
104
  view(initialView) {
103
105
  let view = initialView;
104
- const unbindDocumentMouseDown = bind(document, {
106
+ const doc = getDocument();
107
+ if (!doc) {
108
+ return {
109
+ update() {},
110
+ destroy() {}
111
+ };
112
+ }
113
+ const unbindDocumentMouseDown = bind(doc, {
105
114
  type: 'mousedown',
106
115
  listener: e => {
107
116
  if (!(e.target instanceof HTMLElement)) {
@@ -143,14 +152,42 @@ export const createSelectionPreservationPlugin = api => () => {
143
152
  });
144
153
  return {
145
154
  update(updateView, prevState) {
146
- var _selectionPreservatio, _selectionPreservatio2, _key$getState, _key$getState2;
147
155
  view = updateView;
148
- const prevPreservedSelection = (_selectionPreservatio = selectionPreservationPluginKey.getState(prevState)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
149
- const currPreservedSelection = (_selectionPreservatio2 = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio2 === void 0 ? void 0 : _selectionPreservatio2.preservedSelection;
150
- const prevActiveNode = (_key$getState = key.getState(prevState)) === null || _key$getState === void 0 ? void 0 : _key$getState.activeNode;
151
- const currActiveNode = (_key$getState2 = key.getState(view.state)) === null || _key$getState2 === void 0 ? void 0 : _key$getState2.activeNode;
152
- if (currPreservedSelection && view.hasFocus() && (!compareSelections(prevPreservedSelection, currPreservedSelection) || prevActiveNode !== currActiveNode)) {
153
- syncDOMSelection(view.state.selection);
156
+
157
+ // [FEATURE FLAG: platform_editor_selection_sync_fix]
158
+ // When enabled, syncs DOM selection even when editor doesn't have focus.
159
+ // This prevents ghost highlighting after moving nodes when block menu is open.
160
+ // To clean up: remove the if-else block and keep only the flag-on behavior.
161
+ if (fg('platform_editor_selection_sync_fix')) {
162
+ var _selectionPreservatio, _selectionPreservatio2, _key$getState, _key$getState2;
163
+ const prevPreservedSelection = (_selectionPreservatio = selectionPreservationPluginKey.getState(prevState)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
164
+ const currPreservedSelection = (_selectionPreservatio2 = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio2 === void 0 ? void 0 : _selectionPreservatio2.preservedSelection;
165
+ const prevActiveNode = (_key$getState = key.getState(prevState)) === null || _key$getState === void 0 ? void 0 : _key$getState.activeNode;
166
+ const currActiveNode = (_key$getState2 = key.getState(view.state)) === null || _key$getState2 === void 0 ? void 0 : _key$getState2.activeNode;
167
+
168
+ // Sync DOM selection when the preserved selection or active node changes
169
+ // AND the document has changed (e.g., nodes moved)
170
+ // This prevents stealing focus during menu navigation while still fixing ghost highlighting
171
+ const hasPreservedSelection = !!currPreservedSelection;
172
+ const preservedSelectionChanged = !compareSelections(prevPreservedSelection, currPreservedSelection);
173
+ const activeNodeChanged = prevActiveNode !== currActiveNode;
174
+ const docChanged = prevState.doc !== view.state.doc;
175
+ const shouldSyncDOMSelection = hasPreservedSelection && (preservedSelectionChanged || activeNodeChanged) && docChanged;
176
+ if (shouldSyncDOMSelection) {
177
+ syncDOMSelection(view.state.selection, view);
178
+ }
179
+ } else {
180
+ var _selectionPreservatio3, _selectionPreservatio4, _key$getState3, _key$getState4;
181
+ // OLD BEHAVIOR (to be removed when flag is cleaned up)
182
+ // Only synced when editor had focus, causing ghost highlighting issues
183
+ const prevPreservedSelection = (_selectionPreservatio3 = selectionPreservationPluginKey.getState(prevState)) === null || _selectionPreservatio3 === void 0 ? void 0 : _selectionPreservatio3.preservedSelection;
184
+ const currPreservedSelection = (_selectionPreservatio4 = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio4 === void 0 ? void 0 : _selectionPreservatio4.preservedSelection;
185
+ const prevActiveNode = (_key$getState3 = key.getState(prevState)) === null || _key$getState3 === void 0 ? void 0 : _key$getState3.activeNode;
186
+ const currActiveNode = (_key$getState4 = key.getState(view.state)) === null || _key$getState4 === void 0 ? void 0 : _key$getState4.activeNode;
187
+ if (currPreservedSelection && view.hasFocus() && (!compareSelections(prevPreservedSelection, currPreservedSelection) || prevActiveNode !== currActiveNode)) {
188
+ // Old syncDOMSelection signature (to be removed)
189
+ syncDOMSelection(view.state.selection);
190
+ }
154
191
  }
155
192
  },
156
193
  destroy() {
@@ -1,3 +1,5 @@
1
+ import { getDocument } from '@atlaskit/browser-apis';
2
+ import { fg } from '@atlaskit/platform-feature-flags';
1
3
  import { selectionPreservationPluginKey } from './plugin-key';
2
4
  /**
3
5
  * Detects if any of the transactions include user-driven selection changes.
@@ -24,23 +26,61 @@ export const compareSelections = (a, b) => {
24
26
  };
25
27
 
26
28
  /**
27
- * Triggers a DOM selection sync by resetting the current native selection range
28
- * only if it is out of sync with the provided ProseMirror selection state.
29
+ * Forces the browser's native selection to match ProseMirror's selection state.
29
30
  *
30
- * This is a necessary workaround to ensure the browser's native selection state
31
- * stays in sync with the preserved selection, particularly after transactions
32
- * that shift document content.
31
+ * This is necessary when the editor doesn't have focus (e.g., when block menu is open)
32
+ * but we still need to update the visual selection after moving nodes. Without this,
33
+ * the browser's native selection remains at the old position, causing ghost highlighting.
33
34
  *
34
- * @param selection The current ProseMirror selection state to compare against.
35
+ * @param selection The current ProseMirror selection state to sync to DOM.
36
+ * @param view The EditorView instance used to convert ProseMirror positions to DOM positions (when feature flag is enabled).
35
37
  */
36
- export const syncDOMSelection = selection => {
37
- const domSelection = window.getSelection();
38
- const domRange = domSelection && domSelection.rangeCount === 1 && domSelection.getRangeAt(0).cloneRange();
39
- const isOutOfSync = domRange && (selection.from !== domRange.startOffset || selection.to !== domRange.endOffset);
40
- if (isOutOfSync) {
41
- // Force the DOM selection to refresh, setting it to the same range
42
- // This will trigger ProseMirror to re-apply its selection logic based on the current state
43
- domSelection.removeAllRanges();
44
- domSelection.addRange(domRange);
38
+ export const syncDOMSelection = (selection, view) => {
39
+ // [FEATURE FLAG: platform_editor_selection_sync_fix]
40
+ // When enabled, uses improved DOM selection syncing with EditorView.
41
+ // To clean up: remove the if-else block, remove the optional view parameter,
42
+ // make view required, and keep only the flag-on behavior.
43
+ if (view && fg('platform_editor_selection_sync_fix')) {
44
+ try {
45
+ const domSelection = window.getSelection();
46
+ if (!domSelection) {
47
+ return;
48
+ }
49
+ const doc = getDocument();
50
+ if (!doc) {
51
+ return;
52
+ }
53
+
54
+ // Convert ProseMirror selection to DOM selection using view.domAtPos
55
+ const anchor = view.domAtPos(selection.anchor);
56
+ const head = view.domAtPos(selection.head);
57
+ if (!anchor || !head) {
58
+ return;
59
+ }
60
+
61
+ // Create a new DOM range from the ProseMirror selection
62
+ const range = doc.createRange();
63
+ range.setStart(anchor.node, anchor.offset);
64
+ range.setEnd(head.node, head.offset);
65
+
66
+ // Update the DOM selection to match ProseMirror's selection
67
+ domSelection.removeAllRanges();
68
+ domSelection.addRange(range);
69
+ } catch {
70
+ // Silently fail if DOM selection sync fails
71
+ // This can happen if positions are invalid or DOM hasn't updated yet
72
+ }
73
+ } else {
74
+ // OLD BEHAVIOR (to be removed when flag is cleaned up)
75
+ // Only checked if selection was out of sync using incorrect offset comparison
76
+ const domSelection = window.getSelection();
77
+ const domRange = domSelection && domSelection.rangeCount === 1 && domSelection.getRangeAt(0).cloneRange();
78
+ const isOutOfSync = domRange && (selection.from !== domRange.startOffset || selection.to !== domRange.endOffset);
79
+ if (isOutOfSync && domSelection && domRange) {
80
+ // Force the DOM selection to refresh, setting it to the same range
81
+ // This will trigger ProseMirror to re-apply its selection logic based on the current state
82
+ domSelection.removeAllRanges();
83
+ domSelection.addRange(domRange);
84
+ }
45
85
  }
46
86
  };
@@ -40,7 +40,7 @@ var shouldCollapseMargin = function shouldCollapseMargin(prevNode, nextNode) {
40
40
  return true;
41
41
  };
42
42
  var getGapAndOffset = function getGapAndOffset(prevNode, nextNode, parentNode) {
43
- var isSyncBlockOffsetPatchEnabled = expValEquals('platform_synced_block', 'isEnabled', true) && fg('platform_synced_block_patch_2');
43
+ var isSyncBlockOffsetPatchEnabled = editorExperiment('platform_synced_block', true) && fg('platform_synced_block_patch_2');
44
44
  if (!prevNode && nextNode) {
45
45
  // first node - adjust for bodied containers
46
46
  var _offset = 0;
@@ -250,7 +250,7 @@ export var dropTargetDecorations = function dropTargetDecorations(newState, api,
250
250
  return false; //not valid pos, so nested not valid either
251
251
  }
252
252
  }
253
- var parentTypesWithEndDropTarget = expValEquals('platform_synced_block', 'isEnabled', true) && fg('platform_synced_block_patch_2') ? PARENT_WITH_END_DROP_TARGET_NEXT : PARENT_WITH_END_DROP_TARGET;
253
+ var parentTypesWithEndDropTarget = editorExperiment('platform_synced_block', true) && fg('platform_synced_block_patch_2') ? PARENT_WITH_END_DROP_TARGET_NEXT : PARENT_WITH_END_DROP_TARGET;
254
254
  if (parent.lastChild === node && !isEmptyParagraph(node) && parentTypesWithEndDropTarget.includes(parent.type.name)) {
255
255
  endPos = pos + node.nodeSize;
256
256
  }
@@ -2,8 +2,10 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
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
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
4
  import { bind } from 'bind-event-listener';
5
+ import { getDocument } from '@atlaskit/browser-apis';
5
6
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
6
7
  import { DRAG_HANDLE_SELECTOR } from '@atlaskit/editor-common/styles';
8
+ import { fg } from '@atlaskit/platform-feature-flags';
7
9
  import { key } from '../main';
8
10
  import { createPreservedSelection, mapPreservedSelection } from '../utils/selection';
9
11
  import { stopPreservingSelection } from './editor-commands';
@@ -103,7 +105,14 @@ export var createSelectionPreservationPlugin = function createSelectionPreservat
103
105
  },
104
106
  view: function view(initialView) {
105
107
  var view = initialView;
106
- var unbindDocumentMouseDown = bind(document, {
108
+ var doc = getDocument();
109
+ if (!doc) {
110
+ return {
111
+ update: function update() {},
112
+ destroy: function destroy() {}
113
+ };
114
+ }
115
+ var unbindDocumentMouseDown = bind(doc, {
107
116
  type: 'mousedown',
108
117
  listener: function listener(e) {
109
118
  if (!(e.target instanceof HTMLElement)) {
@@ -144,14 +153,42 @@ export var createSelectionPreservationPlugin = function createSelectionPreservat
144
153
  });
145
154
  return {
146
155
  update: function update(updateView, prevState) {
147
- var _selectionPreservatio, _selectionPreservatio2, _key$getState, _key$getState2;
148
156
  view = updateView;
149
- var prevPreservedSelection = (_selectionPreservatio = selectionPreservationPluginKey.getState(prevState)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
150
- var currPreservedSelection = (_selectionPreservatio2 = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio2 === void 0 ? void 0 : _selectionPreservatio2.preservedSelection;
151
- var prevActiveNode = (_key$getState = key.getState(prevState)) === null || _key$getState === void 0 ? void 0 : _key$getState.activeNode;
152
- var currActiveNode = (_key$getState2 = key.getState(view.state)) === null || _key$getState2 === void 0 ? void 0 : _key$getState2.activeNode;
153
- if (currPreservedSelection && view.hasFocus() && (!compareSelections(prevPreservedSelection, currPreservedSelection) || prevActiveNode !== currActiveNode)) {
154
- syncDOMSelection(view.state.selection);
157
+
158
+ // [FEATURE FLAG: platform_editor_selection_sync_fix]
159
+ // When enabled, syncs DOM selection even when editor doesn't have focus.
160
+ // This prevents ghost highlighting after moving nodes when block menu is open.
161
+ // To clean up: remove the if-else block and keep only the flag-on behavior.
162
+ if (fg('platform_editor_selection_sync_fix')) {
163
+ var _selectionPreservatio, _selectionPreservatio2, _key$getState, _key$getState2;
164
+ var prevPreservedSelection = (_selectionPreservatio = selectionPreservationPluginKey.getState(prevState)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
165
+ var currPreservedSelection = (_selectionPreservatio2 = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio2 === void 0 ? void 0 : _selectionPreservatio2.preservedSelection;
166
+ var prevActiveNode = (_key$getState = key.getState(prevState)) === null || _key$getState === void 0 ? void 0 : _key$getState.activeNode;
167
+ var currActiveNode = (_key$getState2 = key.getState(view.state)) === null || _key$getState2 === void 0 ? void 0 : _key$getState2.activeNode;
168
+
169
+ // Sync DOM selection when the preserved selection or active node changes
170
+ // AND the document has changed (e.g., nodes moved)
171
+ // This prevents stealing focus during menu navigation while still fixing ghost highlighting
172
+ var hasPreservedSelection = !!currPreservedSelection;
173
+ var preservedSelectionChanged = !compareSelections(prevPreservedSelection, currPreservedSelection);
174
+ var activeNodeChanged = prevActiveNode !== currActiveNode;
175
+ var docChanged = prevState.doc !== view.state.doc;
176
+ var shouldSyncDOMSelection = hasPreservedSelection && (preservedSelectionChanged || activeNodeChanged) && docChanged;
177
+ if (shouldSyncDOMSelection) {
178
+ syncDOMSelection(view.state.selection, view);
179
+ }
180
+ } else {
181
+ var _selectionPreservatio3, _selectionPreservatio4, _key$getState3, _key$getState4;
182
+ // OLD BEHAVIOR (to be removed when flag is cleaned up)
183
+ // Only synced when editor had focus, causing ghost highlighting issues
184
+ var _prevPreservedSelection = (_selectionPreservatio3 = selectionPreservationPluginKey.getState(prevState)) === null || _selectionPreservatio3 === void 0 ? void 0 : _selectionPreservatio3.preservedSelection;
185
+ var _currPreservedSelection = (_selectionPreservatio4 = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio4 === void 0 ? void 0 : _selectionPreservatio4.preservedSelection;
186
+ var _prevActiveNode = (_key$getState3 = key.getState(prevState)) === null || _key$getState3 === void 0 ? void 0 : _key$getState3.activeNode;
187
+ var _currActiveNode = (_key$getState4 = key.getState(view.state)) === null || _key$getState4 === void 0 ? void 0 : _key$getState4.activeNode;
188
+ if (_currPreservedSelection && view.hasFocus() && (!compareSelections(_prevPreservedSelection, _currPreservedSelection) || _prevActiveNode !== _currActiveNode)) {
189
+ // Old syncDOMSelection signature (to be removed)
190
+ syncDOMSelection(view.state.selection);
191
+ }
155
192
  }
156
193
  },
157
194
  destroy: function destroy() {
@@ -1,3 +1,5 @@
1
+ import { getDocument } from '@atlaskit/browser-apis';
2
+ import { fg } from '@atlaskit/platform-feature-flags';
1
3
  import { selectionPreservationPluginKey } from './plugin-key';
2
4
  /**
3
5
  * Detects if any of the transactions include user-driven selection changes.
@@ -26,23 +28,61 @@ export var compareSelections = function compareSelections(a, b) {
26
28
  };
27
29
 
28
30
  /**
29
- * Triggers a DOM selection sync by resetting the current native selection range
30
- * only if it is out of sync with the provided ProseMirror selection state.
31
+ * Forces the browser's native selection to match ProseMirror's selection state.
31
32
  *
32
- * This is a necessary workaround to ensure the browser's native selection state
33
- * stays in sync with the preserved selection, particularly after transactions
34
- * that shift document content.
33
+ * This is necessary when the editor doesn't have focus (e.g., when block menu is open)
34
+ * but we still need to update the visual selection after moving nodes. Without this,
35
+ * the browser's native selection remains at the old position, causing ghost highlighting.
35
36
  *
36
- * @param selection The current ProseMirror selection state to compare against.
37
+ * @param selection The current ProseMirror selection state to sync to DOM.
38
+ * @param view The EditorView instance used to convert ProseMirror positions to DOM positions (when feature flag is enabled).
37
39
  */
38
- export var syncDOMSelection = function syncDOMSelection(selection) {
39
- var domSelection = window.getSelection();
40
- var domRange = domSelection && domSelection.rangeCount === 1 && domSelection.getRangeAt(0).cloneRange();
41
- var isOutOfSync = domRange && (selection.from !== domRange.startOffset || selection.to !== domRange.endOffset);
42
- if (isOutOfSync) {
43
- // Force the DOM selection to refresh, setting it to the same range
44
- // This will trigger ProseMirror to re-apply its selection logic based on the current state
45
- domSelection.removeAllRanges();
46
- domSelection.addRange(domRange);
40
+ export var syncDOMSelection = function syncDOMSelection(selection, view) {
41
+ // [FEATURE FLAG: platform_editor_selection_sync_fix]
42
+ // When enabled, uses improved DOM selection syncing with EditorView.
43
+ // To clean up: remove the if-else block, remove the optional view parameter,
44
+ // make view required, and keep only the flag-on behavior.
45
+ if (view && fg('platform_editor_selection_sync_fix')) {
46
+ try {
47
+ var domSelection = window.getSelection();
48
+ if (!domSelection) {
49
+ return;
50
+ }
51
+ var doc = getDocument();
52
+ if (!doc) {
53
+ return;
54
+ }
55
+
56
+ // Convert ProseMirror selection to DOM selection using view.domAtPos
57
+ var anchor = view.domAtPos(selection.anchor);
58
+ var head = view.domAtPos(selection.head);
59
+ if (!anchor || !head) {
60
+ return;
61
+ }
62
+
63
+ // Create a new DOM range from the ProseMirror selection
64
+ var range = doc.createRange();
65
+ range.setStart(anchor.node, anchor.offset);
66
+ range.setEnd(head.node, head.offset);
67
+
68
+ // Update the DOM selection to match ProseMirror's selection
69
+ domSelection.removeAllRanges();
70
+ domSelection.addRange(range);
71
+ } catch (_unused) {
72
+ // Silently fail if DOM selection sync fails
73
+ // This can happen if positions are invalid or DOM hasn't updated yet
74
+ }
75
+ } else {
76
+ // OLD BEHAVIOR (to be removed when flag is cleaned up)
77
+ // Only checked if selection was out of sync using incorrect offset comparison
78
+ var _domSelection = window.getSelection();
79
+ var domRange = _domSelection && _domSelection.rangeCount === 1 && _domSelection.getRangeAt(0).cloneRange();
80
+ var isOutOfSync = domRange && (selection.from !== domRange.startOffset || selection.to !== domRange.endOffset);
81
+ if (isOutOfSync && _domSelection && domRange) {
82
+ // Force the DOM selection to refresh, setting it to the same range
83
+ // This will trigger ProseMirror to re-apply its selection logic based on the current state
84
+ _domSelection.removeAllRanges();
85
+ _domSelection.addRange(domRange);
86
+ }
47
87
  }
48
88
  };
@@ -1,4 +1,5 @@
1
1
  import type { ReadonlyTransaction, Selection, Transaction } from '@atlaskit/editor-prosemirror/state';
2
+ import type { EditorView } from '@atlaskit/editor-prosemirror/view';
2
3
  import type { SelectionPreservationMeta } from './types';
3
4
  /**
4
5
  * Detects if any of the transactions include user-driven selection changes.
@@ -17,13 +18,13 @@ export declare const getSelectionPreservationMeta: (tr: Transaction | ReadonlyTr
17
18
  */
18
19
  export declare const compareSelections: (a?: Selection, b?: Selection) => boolean;
19
20
  /**
20
- * Triggers a DOM selection sync by resetting the current native selection range
21
- * only if it is out of sync with the provided ProseMirror selection state.
21
+ * Forces the browser's native selection to match ProseMirror's selection state.
22
22
  *
23
- * This is a necessary workaround to ensure the browser's native selection state
24
- * stays in sync with the preserved selection, particularly after transactions
25
- * that shift document content.
23
+ * This is necessary when the editor doesn't have focus (e.g., when block menu is open)
24
+ * but we still need to update the visual selection after moving nodes. Without this,
25
+ * the browser's native selection remains at the old position, causing ghost highlighting.
26
26
  *
27
- * @param selection The current ProseMirror selection state to compare against.
27
+ * @param selection The current ProseMirror selection state to sync to DOM.
28
+ * @param view The EditorView instance used to convert ProseMirror positions to DOM positions (when feature flag is enabled).
28
29
  */
29
- export declare const syncDOMSelection: (selection: Selection) => void;
30
+ export declare const syncDOMSelection: (selection: Selection, view?: EditorView) => void;
@@ -1,4 +1,5 @@
1
1
  import type { ReadonlyTransaction, Selection, Transaction } from '@atlaskit/editor-prosemirror/state';
2
+ import type { EditorView } from '@atlaskit/editor-prosemirror/view';
2
3
  import type { SelectionPreservationMeta } from './types';
3
4
  /**
4
5
  * Detects if any of the transactions include user-driven selection changes.
@@ -17,13 +18,13 @@ export declare const getSelectionPreservationMeta: (tr: Transaction | ReadonlyTr
17
18
  */
18
19
  export declare const compareSelections: (a?: Selection, b?: Selection) => boolean;
19
20
  /**
20
- * Triggers a DOM selection sync by resetting the current native selection range
21
- * only if it is out of sync with the provided ProseMirror selection state.
21
+ * Forces the browser's native selection to match ProseMirror's selection state.
22
22
  *
23
- * This is a necessary workaround to ensure the browser's native selection state
24
- * stays in sync with the preserved selection, particularly after transactions
25
- * that shift document content.
23
+ * This is necessary when the editor doesn't have focus (e.g., when block menu is open)
24
+ * but we still need to update the visual selection after moving nodes. Without this,
25
+ * the browser's native selection remains at the old position, causing ghost highlighting.
26
26
  *
27
- * @param selection The current ProseMirror selection state to compare against.
27
+ * @param selection The current ProseMirror selection state to sync to DOM.
28
+ * @param view The EditorView instance used to convert ProseMirror positions to DOM positions (when feature flag is enabled).
28
29
  */
29
- export declare const syncDOMSelection: (selection: Selection) => void;
30
+ export declare const syncDOMSelection: (selection: Selection, view?: EditorView) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-block-controls",
3
- "version": "8.2.1",
3
+ "version": "8.3.1",
4
4
  "description": "Block controls plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -54,7 +54,7 @@
54
54
  "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^3.2.0",
55
55
  "@atlaskit/primitives": "^18.0.0",
56
56
  "@atlaskit/theme": "^21.0.0",
57
- "@atlaskit/tmp-editor-statsig": "^23.0.0",
57
+ "@atlaskit/tmp-editor-statsig": "^23.2.0",
58
58
  "@atlaskit/tokens": "^11.0.0",
59
59
  "@atlaskit/tooltip": "^20.14.0",
60
60
  "@babel/runtime": "^7.0.0",
@@ -148,6 +148,9 @@
148
148
  },
149
149
  "platform_synced_block_patch_2": {
150
150
  "type": "boolean"
151
+ },
152
+ "platform_editor_selection_sync_fix": {
153
+ "type": "boolean"
151
154
  }
152
155
  }
153
156
  }