@atlaskit/editor-plugin-block-controls 20.0.3 → 20.1.0

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 (107) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cjs/blockControlsPlugin.js +53 -17
  3. package/dist/cjs/editor-commands/show-drag-handle.js +17 -5
  4. package/dist/cjs/pm-plugins/decorations-anchor.js +2 -1
  5. package/dist/cjs/pm-plugins/decorations-drop-target-active.js +2 -1
  6. package/dist/cjs/pm-plugins/decorations-drop-target.js +2 -1
  7. package/dist/cjs/pm-plugins/decorations-surface-anchor.js +72 -0
  8. package/dist/cjs/pm-plugins/handle-mouse-down.js +2 -1
  9. package/dist/cjs/pm-plugins/handle-mouse-over.js +3 -2
  10. package/dist/cjs/pm-plugins/handle-sparse-mouse-over.js +125 -0
  11. package/dist/cjs/pm-plugins/main.js +66 -54
  12. package/dist/cjs/pm-plugins/sparse-surfaces.js +49 -0
  13. package/dist/cjs/pm-plugins/utils/sparse-surface-candidates.js +419 -0
  14. package/dist/cjs/pm-plugins/utils/sparse-surfaces-mapping.js +108 -0
  15. package/dist/cjs/pm-plugins/utils/sparse-surfaces-state.js +53 -0
  16. package/dist/cjs/pm-plugins/utils/sparse-surfaces-view.js +134 -0
  17. package/dist/cjs/pm-plugins/utils/surface-viewport.js +223 -0
  18. package/dist/cjs/pm-plugins/utils/transactions.js +5 -0
  19. package/dist/cjs/ui/block-controls-left-surfaces.js +31 -25
  20. package/dist/cjs/ui/block-controls-right-surfaces.js +31 -25
  21. package/dist/cjs/ui/block-controls-surface-context.js +1 -0
  22. package/dist/cjs/ui/block-controls-surface-drag-handle-registration.js +6 -7
  23. package/dist/cjs/ui/block-controls-surface-drag-handle.js +6 -2
  24. package/dist/cjs/ui/drag-handle.js +5 -3
  25. package/dist/cjs/ui/drop-target-layout.js +3 -2
  26. package/dist/cjs/ui/drop-target.js +3 -2
  27. package/dist/cjs/ui/global-styles.js +69 -27
  28. package/dist/cjs/ui/inline-drop-target.js +6 -5
  29. package/dist/cjs/ui/utils/anchor-name.js +2 -1
  30. package/dist/cjs/ui/utils/dom-attr-name.js +3 -2
  31. package/dist/cjs/ui/utils/get-surface-placement.js +2 -1
  32. package/dist/es2019/blockControlsPlugin.js +42 -8
  33. package/dist/es2019/editor-commands/show-drag-handle.js +14 -2
  34. package/dist/es2019/pm-plugins/decorations-anchor.js +2 -1
  35. package/dist/es2019/pm-plugins/decorations-drop-target-active.js +2 -1
  36. package/dist/es2019/pm-plugins/decorations-drop-target.js +2 -1
  37. package/dist/es2019/pm-plugins/decorations-surface-anchor.js +56 -0
  38. package/dist/es2019/pm-plugins/handle-mouse-down.js +2 -1
  39. package/dist/es2019/pm-plugins/handle-mouse-over.js +3 -2
  40. package/dist/es2019/pm-plugins/handle-sparse-mouse-over.js +116 -0
  41. package/dist/es2019/pm-plugins/main.js +70 -54
  42. package/dist/es2019/pm-plugins/sparse-surfaces.js +37 -0
  43. package/dist/es2019/pm-plugins/utils/sparse-surface-candidates.js +305 -0
  44. package/dist/es2019/pm-plugins/utils/sparse-surfaces-mapping.js +78 -0
  45. package/dist/es2019/pm-plugins/utils/sparse-surfaces-state.js +39 -0
  46. package/dist/es2019/pm-plugins/utils/sparse-surfaces-view.js +121 -0
  47. package/dist/es2019/pm-plugins/utils/surface-viewport.js +192 -0
  48. package/dist/es2019/pm-plugins/utils/transactions.js +5 -0
  49. package/dist/es2019/ui/block-controls-left-surfaces.js +21 -14
  50. package/dist/es2019/ui/block-controls-right-surfaces.js +21 -14
  51. package/dist/es2019/ui/block-controls-surface-context.js +1 -0
  52. package/dist/es2019/ui/block-controls-surface-drag-handle-registration.js +4 -5
  53. package/dist/es2019/ui/block-controls-surface-drag-handle.js +6 -2
  54. package/dist/es2019/ui/drag-handle.js +5 -3
  55. package/dist/es2019/ui/drop-target-layout.js +3 -2
  56. package/dist/es2019/ui/drop-target.js +3 -2
  57. package/dist/es2019/ui/global-styles.js +67 -28
  58. package/dist/es2019/ui/inline-drop-target.js +6 -5
  59. package/dist/es2019/ui/utils/anchor-name.js +2 -1
  60. package/dist/es2019/ui/utils/dom-attr-name.js +3 -2
  61. package/dist/es2019/ui/utils/get-surface-placement.js +2 -1
  62. package/dist/esm/blockControlsPlugin.js +53 -17
  63. package/dist/esm/editor-commands/show-drag-handle.js +17 -5
  64. package/dist/esm/pm-plugins/decorations-anchor.js +2 -1
  65. package/dist/esm/pm-plugins/decorations-drop-target-active.js +2 -1
  66. package/dist/esm/pm-plugins/decorations-drop-target.js +2 -1
  67. package/dist/esm/pm-plugins/decorations-surface-anchor.js +65 -0
  68. package/dist/esm/pm-plugins/handle-mouse-down.js +2 -1
  69. package/dist/esm/pm-plugins/handle-mouse-over.js +3 -2
  70. package/dist/esm/pm-plugins/handle-sparse-mouse-over.js +119 -0
  71. package/dist/esm/pm-plugins/main.js +66 -54
  72. package/dist/esm/pm-plugins/sparse-surfaces.js +43 -0
  73. package/dist/esm/pm-plugins/utils/sparse-surface-candidates.js +413 -0
  74. package/dist/esm/pm-plugins/utils/sparse-surfaces-mapping.js +101 -0
  75. package/dist/esm/pm-plugins/utils/sparse-surfaces-state.js +46 -0
  76. package/dist/esm/pm-plugins/utils/sparse-surfaces-view.js +127 -0
  77. package/dist/esm/pm-plugins/utils/surface-viewport.js +217 -0
  78. package/dist/esm/pm-plugins/utils/transactions.js +5 -0
  79. package/dist/esm/ui/block-controls-left-surfaces.js +33 -27
  80. package/dist/esm/ui/block-controls-right-surfaces.js +33 -27
  81. package/dist/esm/ui/block-controls-surface-context.js +1 -0
  82. package/dist/esm/ui/block-controls-surface-drag-handle-registration.js +7 -8
  83. package/dist/esm/ui/block-controls-surface-drag-handle.js +6 -2
  84. package/dist/esm/ui/drag-handle.js +5 -3
  85. package/dist/esm/ui/drop-target-layout.js +3 -2
  86. package/dist/esm/ui/drop-target.js +3 -2
  87. package/dist/esm/ui/global-styles.js +70 -28
  88. package/dist/esm/ui/inline-drop-target.js +6 -5
  89. package/dist/esm/ui/utils/anchor-name.js +2 -1
  90. package/dist/esm/ui/utils/dom-attr-name.js +3 -2
  91. package/dist/esm/ui/utils/get-surface-placement.js +2 -1
  92. package/dist/types/blockControlsPluginType.d.ts +10 -3
  93. package/dist/types/pm-plugins/decorations-surface-anchor.d.ts +22 -0
  94. package/dist/types/pm-plugins/handle-sparse-mouse-over.d.ts +5 -0
  95. package/dist/types/pm-plugins/main.d.ts +2 -9
  96. package/dist/types/pm-plugins/sparse-surfaces.d.ts +8 -0
  97. package/dist/types/pm-plugins/utils/sparse-surface-candidates.d.ts +89 -0
  98. package/dist/types/pm-plugins/utils/sparse-surfaces-mapping.d.ts +20 -0
  99. package/dist/types/pm-plugins/utils/sparse-surfaces-state.d.ts +27 -0
  100. package/dist/types/pm-plugins/utils/sparse-surfaces-view.d.ts +15 -0
  101. package/dist/types/pm-plugins/utils/surface-viewport.d.ts +10 -0
  102. package/dist/types/ui/utils/get-surface-placement.d.ts +3 -2
  103. package/package.json +5 -5
  104. package/dist/cjs/pm-plugins/utils/surface-node-positions.js +0 -100
  105. package/dist/es2019/pm-plugins/utils/surface-node-positions.js +0 -78
  106. package/dist/esm/pm-plugins/utils/surface-node-positions.js +0 -93
  107. package/dist/types/pm-plugins/utils/surface-node-positions.d.ts +0 -14
@@ -0,0 +1,192 @@
1
+ const NODE_NAME_ATTRIBUTE = 'data-prosemirror-node-name';
2
+ /**
3
+ * Tracks block DOM nodes that intersect the browser viewport. The browser owns clipping and
4
+ * visibility decisions; this helper only translates observer targets back to PM positions.
5
+ */
6
+ export const observeSurfaceViewport = (view, onChange) => {
7
+ const win = view.dom.ownerDocument.defaultView;
8
+ if (!win || typeof win.IntersectionObserver === 'undefined') {
9
+ return {
10
+ update: () => {},
11
+ destroy: () => {}
12
+ };
13
+ }
14
+ const targets = new Map();
15
+ const visibleTargets = new Set();
16
+ let previousPositions = [];
17
+ let previousDoc = view.state.doc;
18
+ let destroyed = false;
19
+ const resolvePosition = element => {
20
+ try {
21
+ const domPosition = view.posAtDOM(element, 0);
22
+ const nodeName = element.getAttribute(NODE_NAME_ATTRIBUTE);
23
+ const directNode = view.state.doc.nodeAt(domPosition);
24
+ if (directNode !== null && directNode !== void 0 && directNode.isBlock && directNode.type.name === nodeName && view.nodeDOM(domPosition) === element) {
25
+ return domPosition;
26
+ }
27
+ const $position = view.state.doc.resolve(domPosition);
28
+ for (let depth = $position.depth; depth > 0; depth--) {
29
+ const node = $position.node(depth);
30
+ if (!node.isBlock || node.type.name !== nodeName) {
31
+ continue;
32
+ }
33
+ const position = $position.before(depth);
34
+ const nodeDOM = view.nodeDOM(position);
35
+ if (nodeDOM === element || nodeDOM instanceof Node && (nodeDOM.contains(element) || element.contains(nodeDOM))) {
36
+ return position;
37
+ }
38
+ }
39
+ return undefined;
40
+ } catch {
41
+ return undefined;
42
+ }
43
+ };
44
+ const getVisiblePositions = () => {
45
+ const positions = new Set();
46
+ visibleTargets.forEach(element => {
47
+ var _targets$get;
48
+ const position = (_targets$get = targets.get(element)) === null || _targets$get === void 0 ? void 0 : _targets$get.position;
49
+ if (position !== undefined) {
50
+ positions.add(position);
51
+ }
52
+ });
53
+ return Array.from(positions).sort((first, second) => first - second);
54
+ };
55
+ const emit = () => {
56
+ if (destroyed) {
57
+ return;
58
+ }
59
+ const positions = getVisiblePositions();
60
+ if (positions.length !== previousPositions.length || positions.some((position, index) => position !== previousPositions[index])) {
61
+ previousPositions = positions;
62
+ onChange(positions);
63
+ }
64
+ };
65
+ const intersectionObserver = new win.IntersectionObserver(entries => {
66
+ let changed = false;
67
+ for (const entry of entries) {
68
+ const element = entry.target;
69
+ const target = targets.get(element);
70
+ if (!target || !view.dom.contains(element)) {
71
+ continue;
72
+ }
73
+ const isIntersecting = entry.isIntersecting;
74
+ if (target.isIntersecting !== isIntersecting) {
75
+ target.isIntersecting = isIntersecting;
76
+ if (isIntersecting) {
77
+ target.position = resolvePosition(element);
78
+ if (target.position !== undefined) {
79
+ visibleTargets.add(element);
80
+ } else {
81
+ target.isIntersecting = false;
82
+ }
83
+ } else {
84
+ visibleTargets.delete(element);
85
+ }
86
+ changed = true;
87
+ }
88
+ }
89
+ if (changed) {
90
+ emit();
91
+ }
92
+ }, {
93
+ root: null,
94
+ threshold: 0
95
+ });
96
+ const removeTarget = element => {
97
+ const target = targets.get(element);
98
+ if (!target) {
99
+ return;
100
+ }
101
+ targets.delete(element);
102
+ visibleTargets.delete(element);
103
+ intersectionObserver.unobserve(element);
104
+ };
105
+ const observeElement = element => {
106
+ var _view$state$schema$no;
107
+ const nodeName = element.getAttribute(NODE_NAME_ATTRIBUTE);
108
+ if (element === view.dom || !nodeName || !((_view$state$schema$no = view.state.schema.nodes[nodeName]) !== null && _view$state$schema$no !== void 0 && _view$state$schema$no.isBlock) || !view.dom.contains(element)) {
109
+ return;
110
+ }
111
+ if (targets.has(element)) {
112
+ return;
113
+ }
114
+ targets.set(element, {
115
+ isIntersecting: false
116
+ });
117
+ intersectionObserver.observe(element);
118
+ };
119
+ const observeSubtree = node => {
120
+ if (node.nodeType !== Node.ELEMENT_NODE) {
121
+ return;
122
+ }
123
+ const element = node;
124
+ observeElement(element);
125
+ const descendants = element.querySelectorAll(`[${NODE_NAME_ATTRIBUTE}]`);
126
+ for (let index = 0; index < descendants.length; index++) {
127
+ observeElement(descendants[index]);
128
+ }
129
+ };
130
+ const unobserveSubtree = node => {
131
+ if (node.nodeType !== Node.ELEMENT_NODE) {
132
+ return;
133
+ }
134
+ const element = node;
135
+ removeTarget(element);
136
+ const descendants = element.querySelectorAll(`[${NODE_NAME_ATTRIBUTE}]`);
137
+ for (let index = 0; index < descendants.length; index++) {
138
+ removeTarget(descendants[index]);
139
+ }
140
+ };
141
+ const mutationObserver = new win.MutationObserver(records => {
142
+ let removedVisibleTarget = false;
143
+ for (const record of records) {
144
+ for (let index = 0; index < record.removedNodes.length; index++) {
145
+ const node = record.removedNodes[index];
146
+ if (node.nodeType === Node.ELEMENT_NODE) {
147
+ if (view.dom.contains(node)) {
148
+ continue;
149
+ }
150
+ const before = visibleTargets.size;
151
+ unobserveSubtree(node);
152
+ removedVisibleTarget ||= before !== visibleTargets.size;
153
+ }
154
+ }
155
+ for (let index = 0; index < record.addedNodes.length; index++) {
156
+ observeSubtree(record.addedNodes[index]);
157
+ }
158
+ }
159
+ if (removedVisibleTarget) {
160
+ emit();
161
+ }
162
+ });
163
+ const initialTargets = view.dom.querySelectorAll(`[${NODE_NAME_ATTRIBUTE}]`);
164
+ for (let index = 0; index < initialTargets.length; index++) {
165
+ observeElement(initialTargets[index]);
166
+ }
167
+ mutationObserver.observe(view.dom, {
168
+ childList: true,
169
+ subtree: true
170
+ });
171
+ return {
172
+ update() {
173
+ if (previousDoc !== view.state.doc) {
174
+ previousDoc = view.state.doc;
175
+ visibleTargets.forEach(element => {
176
+ const target = targets.get(element);
177
+ if (target) {
178
+ target.position = resolvePosition(element);
179
+ }
180
+ });
181
+ emit();
182
+ }
183
+ },
184
+ destroy() {
185
+ destroyed = true;
186
+ mutationObserver.disconnect();
187
+ intersectionObserver.disconnect();
188
+ targets.clear();
189
+ visibleTargets.clear();
190
+ }
191
+ };
192
+ };
@@ -1,4 +1,5 @@
1
1
  import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
2
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
2
3
  /**
3
4
  * Checks if step adds inline char
4
5
  * @param s
@@ -28,6 +29,10 @@ export const isStepDelete = s => {
28
29
  const isStepContentReplacedWithAnotherOfSameSize = s => {
29
30
  if (s instanceof ReplaceAroundStep) {
30
31
  const replacedContentSize = s.to - s.from;
32
+ if (isExperimentEnabled('platform_editor_block_control_migration')) {
33
+ const insertedContentSize = s.slice.size + (s.gapTo - s.gapFrom);
34
+ return insertedContentSize === replacedContentSize;
35
+ }
31
36
  return s.slice.content.size === replacedContentSize;
32
37
  }
33
38
  return false;
@@ -11,12 +11,13 @@ import { createSurfaceContext } from '@atlaskit/editor-ui-control-model/create-s
11
11
  import { willSurfaceRender } from '@atlaskit/editor-ui-control-model/surface-renderer';
12
12
  import { getNodeTypeWithLevel } from '../pm-plugins/decorations-common';
13
13
  import { BlockControlsLeftSurface } from './block-controls-left-surface';
14
- import { createBlockControlsSurfaceContext, createBlockControlsSurfaceContextForPosition } from './block-controls-surface-context';
14
+ import { createBlockControlsSurfaceContextForPosition } from './block-controls-surface-context';
15
15
  import { getBlockControlsSurfaceTargets } from './block-controls-surface-targets';
16
- import { getNodeContentElement, hasInnerContentContainer } from './utils/get-node-content-element';
16
+ import { getNodeContentElement } from './utils/get-node-content-element';
17
17
  import { getAbsoluteSurfacePlacement, getAnchoredSurfacePlacement, getSurfacePlacement, toMeasuredSurfaceWrapperPlacement } from './utils/get-surface-placement';
18
18
  import { hasSurfaceControls } from './utils/has-surface-controls';
19
19
  const EMPTY_SURFACE_POSITIONS = [];
20
+ const EMPTY_SURFACE_ANCHORS = new Map();
20
21
  const EMPTY_SURFACE_COMPONENTS = [];
21
22
  const EMPTY_MEASURED_SURFACES = [];
22
23
  export const BlockControlsLeftSurfaces = ({
@@ -26,19 +27,28 @@ export const BlockControlsLeftSurfaces = ({
26
27
  var _api$uiControlRegistr, _api$uiControlRegistr2;
27
28
  const {
28
29
  activeNode,
30
+ surfaceAnchors,
31
+ surfaceActiveNodes,
29
32
  surfaceNodePositions
30
33
  } = useSharedPluginStateWithSelector(api, ['blockControls'], states => {
31
- var _states$blockControls, _states$blockControls2, _states$blockControls3;
34
+ var _states$blockControls, _states$blockControls2, _states$blockControls3, _states$blockControls4, _states$blockControls5, _states$blockControls6;
32
35
  return {
33
36
  activeNode: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.activeNode,
34
- surfaceNodePositions: (_states$blockControls2 = (_states$blockControls3 = states.blockControlsState) === null || _states$blockControls3 === void 0 ? void 0 : _states$blockControls3.surfaceNodePositions) !== null && _states$blockControls2 !== void 0 ? _states$blockControls2 : EMPTY_SURFACE_POSITIONS
37
+ surfaceActiveNodes: (_states$blockControls2 = states.blockControlsState) === null || _states$blockControls2 === void 0 ? void 0 : _states$blockControls2.surfaceActiveNodes,
38
+ surfaceAnchors: (_states$blockControls3 = (_states$blockControls4 = states.blockControlsState) === null || _states$blockControls4 === void 0 ? void 0 : _states$blockControls4.surfaceAnchors) !== null && _states$blockControls3 !== void 0 ? _states$blockControls3 : EMPTY_SURFACE_ANCHORS,
39
+ surfaceNodePositions: (_states$blockControls5 = (_states$blockControls6 = states.blockControlsState) === null || _states$blockControls6 === void 0 ? void 0 : _states$blockControls6.surfaceNodePositions) !== null && _states$blockControls5 !== void 0 ? _states$blockControls5 : EMPTY_SURFACE_POSITIONS
35
40
  };
36
41
  });
37
42
  const components = (_api$uiControlRegistr = (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(BLOCK_CONTROLS_LEFT_SURFACE)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : EMPTY_SURFACE_COMPONENTS;
38
- const targets = useMemo(() => getBlockControlsSurfaceTargets(activeNode, surfaceNodePositions), [activeNode, surfaceNodePositions]);
43
+ const targets = useMemo(() => {
44
+ const candidates = new Set(surfaceNodePositions);
45
+ return getBlockControlsSurfaceTargets(activeNode, surfaceNodePositions).filter(({
46
+ position
47
+ }) => candidates.has(position));
48
+ }, [activeNode, surfaceNodePositions]);
39
49
  const surfaces = useMemo(() => targets.flatMap(target => {
40
- var _api$core;
41
- const context = target.source === 'stored' ? createBlockControlsSurfaceContextForPosition(editorView.state, target.position, activeNode) : createBlockControlsSurfaceContext(editorView, activeNode, target.source);
50
+ var _surfaceActiveNodes$g;
51
+ const context = createBlockControlsSurfaceContextForPosition(editorView.state, target.position, (_surfaceActiveNodes$g = surfaceActiveNodes === null || surfaceActiveNodes === void 0 ? void 0 : surfaceActiveNodes.get(target.position)) !== null && _surfaceActiveNodes$g !== void 0 ? _surfaceActiveNodes$g : activeNode);
42
52
  if (!context) {
43
53
  return [];
44
54
  }
@@ -46,19 +56,15 @@ export const BlockControlsLeftSurfaces = ({
46
56
  if (!hasSurfaceControls(components) || !willSurfaceRender(components, BLOCK_CONTROLS_LEFT_SURFACE, surfaceContext)) {
47
57
  return [];
48
58
  }
49
- // Resolved here rather than in the measurement effect so an anchored surface is placed
50
- // in the same render that creates it — it never needs a second pass to find its node.
51
- //
52
- // At the moment, we only expect top-level nodes to have persistent controls.
53
- // See surfaceAnchorStyles in global-styles
54
- const anchorName = isCSSAnchorSupported() && context.targetNode.parentType === 'doc' && !hasInnerContentContainer(context.targetNode.type.name) ? (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.getAnchorIdForNode(context.targetNode.node, context.targetNode.pos) : undefined;
59
+ // The cache supplies the same anchor names used by ProseMirror decorations.
60
+ const anchorName = isCSSAnchorSupported() ? surfaceAnchors.get(target.position) : undefined;
55
61
  return [{
56
62
  ...target,
57
63
  anchorName,
58
64
  blockControlsContext: context,
59
65
  surfaceContext
60
66
  }];
61
- }), [activeNode, api, components, editorView, targets]);
67
+ }), [activeNode, components, editorView, targets, surfaceAnchors, surfaceActiveNodes]);
62
68
  /**
63
69
  * Placements that need no measuring, so they are derived rather than stored: every inset is an
64
70
  * `anchor()` the browser re-resolves on every layout change.
@@ -71,6 +77,7 @@ export const BlockControlsLeftSurfaces = ({
71
77
  anchorName,
72
78
  nodeType: blockControlsContext.targetNode.type.name,
73
79
  nodeTypeWithLevel: getNodeTypeWithLevel(blockControlsContext.targetNode.node),
80
+ layout: blockControlsContext.targetNode.node.attrs.layout,
74
81
  parentNodeType: blockControlsContext.targetNode.parentType === 'doc' ? undefined : blockControlsContext.targetNode.parentType,
75
82
  side: 'left'
76
83
  })]] : [])), [surfaces]);
@@ -11,12 +11,13 @@ import { createSurfaceContext } from '@atlaskit/editor-ui-control-model/create-s
11
11
  import { willSurfaceRender } from '@atlaskit/editor-ui-control-model/surface-renderer';
12
12
  import { getNodeTypeWithLevel } from '../pm-plugins/decorations-common';
13
13
  import { BlockControlsRightSurface } from './block-controls-right-surface';
14
- import { createBlockControlsSurfaceContext, createBlockControlsSurfaceContextForPosition } from './block-controls-surface-context';
14
+ import { createBlockControlsSurfaceContextForPosition } from './block-controls-surface-context';
15
15
  import { getBlockControlsSurfaceTargets } from './block-controls-surface-targets';
16
- import { getNodeContentElement, hasInnerContentContainer } from './utils/get-node-content-element';
16
+ import { getNodeContentElement } from './utils/get-node-content-element';
17
17
  import { getAbsoluteSurfacePlacement, getAnchoredSurfacePlacement, getSurfacePlacement, toMeasuredSurfaceWrapperPlacement } from './utils/get-surface-placement';
18
18
  import { hasSurfaceControls } from './utils/has-surface-controls';
19
19
  const EMPTY_SURFACE_POSITIONS = [];
20
+ const EMPTY_SURFACE_ANCHORS = new Map();
20
21
  const EMPTY_SURFACE_COMPONENTS = [];
21
22
  const EMPTY_MEASURED_SURFACES = [];
22
23
  /**
@@ -35,19 +36,28 @@ export const BlockControlsRightSurfaces = ({
35
36
  var _api$uiControlRegistr, _api$uiControlRegistr2;
36
37
  const {
37
38
  activeNode,
39
+ surfaceAnchors,
40
+ surfaceActiveNodes,
38
41
  surfaceNodePositions
39
42
  } = useSharedPluginStateWithSelector(api, ['blockControls'], states => {
40
- var _states$blockControls, _states$blockControls2, _states$blockControls3;
43
+ var _states$blockControls, _states$blockControls2, _states$blockControls3, _states$blockControls4, _states$blockControls5, _states$blockControls6;
41
44
  return {
42
45
  activeNode: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.activeNode,
43
- surfaceNodePositions: (_states$blockControls2 = (_states$blockControls3 = states.blockControlsState) === null || _states$blockControls3 === void 0 ? void 0 : _states$blockControls3.surfaceNodePositions) !== null && _states$blockControls2 !== void 0 ? _states$blockControls2 : EMPTY_SURFACE_POSITIONS
46
+ surfaceActiveNodes: (_states$blockControls2 = states.blockControlsState) === null || _states$blockControls2 === void 0 ? void 0 : _states$blockControls2.surfaceActiveNodes,
47
+ surfaceAnchors: (_states$blockControls3 = (_states$blockControls4 = states.blockControlsState) === null || _states$blockControls4 === void 0 ? void 0 : _states$blockControls4.surfaceAnchors) !== null && _states$blockControls3 !== void 0 ? _states$blockControls3 : EMPTY_SURFACE_ANCHORS,
48
+ surfaceNodePositions: (_states$blockControls5 = (_states$blockControls6 = states.blockControlsState) === null || _states$blockControls6 === void 0 ? void 0 : _states$blockControls6.surfaceNodePositions) !== null && _states$blockControls5 !== void 0 ? _states$blockControls5 : EMPTY_SURFACE_POSITIONS
44
49
  };
45
50
  });
46
51
  const components = (_api$uiControlRegistr = (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(BLOCK_CONTROLS_RIGHT_SURFACE)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : EMPTY_SURFACE_COMPONENTS;
47
- const targets = useMemo(() => getBlockControlsSurfaceTargets(activeNode, surfaceNodePositions), [activeNode, surfaceNodePositions]);
52
+ const targets = useMemo(() => {
53
+ const candidates = new Set(surfaceNodePositions);
54
+ return getBlockControlsSurfaceTargets(activeNode, surfaceNodePositions).filter(({
55
+ position
56
+ }) => candidates.has(position));
57
+ }, [activeNode, surfaceNodePositions]);
48
58
  const surfaces = useMemo(() => targets.flatMap(target => {
49
- var _api$core;
50
- const context = target.source === 'stored' ? createBlockControlsSurfaceContextForPosition(editorView.state, target.position, activeNode) : createBlockControlsSurfaceContext(editorView, activeNode, target.source);
59
+ var _surfaceActiveNodes$g;
60
+ const context = createBlockControlsSurfaceContextForPosition(editorView.state, target.position, (_surfaceActiveNodes$g = surfaceActiveNodes === null || surfaceActiveNodes === void 0 ? void 0 : surfaceActiveNodes.get(target.position)) !== null && _surfaceActiveNodes$g !== void 0 ? _surfaceActiveNodes$g : activeNode);
51
61
  if (!context) {
52
62
  return [];
53
63
  }
@@ -55,19 +65,15 @@ export const BlockControlsRightSurfaces = ({
55
65
  if (!hasSurfaceControls(components) || !willSurfaceRender(components, BLOCK_CONTROLS_RIGHT_SURFACE, surfaceContext)) {
56
66
  return [];
57
67
  }
58
- // Resolved here rather than in the measurement effect so an anchored surface is placed
59
- // in the same render that creates it — it never needs a second pass to find its node.
60
- //
61
- // At the moment, we only expect top-level nodes to have persistent controls.
62
- // See surfaceAnchorStyles in global-styles
63
- const anchorName = isCSSAnchorSupported() && context.targetNode.parentType === 'doc' && !hasInnerContentContainer(context.targetNode.type.name) ? (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.getAnchorIdForNode(context.targetNode.node, context.targetNode.pos) : undefined;
68
+ // The cache supplies the same anchor names used by ProseMirror decorations.
69
+ const anchorName = isCSSAnchorSupported() ? surfaceAnchors.get(target.position) : undefined;
64
70
  return [{
65
71
  ...target,
66
72
  anchorName,
67
73
  blockControlsContext: context,
68
74
  surfaceContext
69
75
  }];
70
- }), [activeNode, api, components, editorView, targets]);
76
+ }), [activeNode, components, editorView, targets, surfaceAnchors, surfaceActiveNodes]);
71
77
  /**
72
78
  * Placements that need no measuring, so they are derived rather than stored: every inset is an
73
79
  * `anchor()` the browser re-resolves on every layout change.
@@ -80,6 +86,7 @@ export const BlockControlsRightSurfaces = ({
80
86
  anchorName,
81
87
  nodeType: blockControlsContext.targetNode.type.name,
82
88
  nodeTypeWithLevel: getNodeTypeWithLevel(blockControlsContext.targetNode.node),
89
+ layout: blockControlsContext.targetNode.node.attrs.layout,
83
90
  parentNodeType: blockControlsContext.targetNode.parentType === 'doc' ? undefined : blockControlsContext.targetNode.parentType,
84
91
  side: 'right'
85
92
  })]] : [])), [surfaces]);
@@ -37,6 +37,7 @@ export const createBlockControlsSurfaceContextForPosition = (state, targetPos, a
37
37
  const rootPos = activeNode ? (_activeNode$rootPos = activeNode.rootPos) !== null && _activeNode$rootPos !== void 0 ? _activeNode$rootPos : activeNode.pos : targetPos;
38
38
  const rootNodeContext = (_getNodeContext = getNodeContext(state, rootPos)) !== null && _getNodeContext !== void 0 ? _getNodeContext : targetNodeContext;
39
39
  return {
40
+ activeControlKey: activeNode === null || activeNode === void 0 ? void 0 : activeNode.controlKey,
40
41
  activeNode: activeNode && activeNodeContext ? {
41
42
  ...activeNodeContext,
42
43
  rootPos,
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { BLOCK_CONTROL_UI_CONTEXT } from '@atlaskit/editor-common/block-controls/block-control-ui-context';
3
- import { BLOCK_CONTROLS_LEFT_GROUP } from '@atlaskit/editor-common/block-controls/surface-keys';
3
+ import { BLOCK_CONTROLS_DRAG_HANDLE, BLOCK_CONTROLS_LEFT_GROUP } from '@atlaskit/editor-common/block-controls/surface-keys';
4
4
  import { BlockControlsSurfaceDragHandle } from './block-controls-surface-drag-handle';
5
5
  export const getBlockControlsSurfaceDragHandleComponents = ({
6
6
  api
@@ -15,12 +15,11 @@ export const getBlockControlsSurfaceDragHandleComponents = ({
15
15
  surfaceContext
16
16
  } = {}) => {
17
17
  const context = surfaceContext === null || surfaceContext === void 0 ? void 0 : surfaceContext.get(BLOCK_CONTROL_UI_CONTEXT);
18
- return !(context !== null && context !== void 0 && context.activeNode) || context.targetNode.pos !== context.activeNode.pos;
18
+ return !(context !== null && context !== void 0 && context.activeNode) || context.targetNode.pos !== context.activeNode.pos || context.activeControlKey !== undefined && context.activeControlKey !== BLOCK_CONTROLS_DRAG_HANDLE.key;
19
19
  },
20
- key: 'block-controls-drag-handle',
20
+ ...BLOCK_CONTROLS_DRAG_HANDLE,
21
21
  parents: [{
22
22
  ...BLOCK_CONTROLS_LEFT_GROUP,
23
23
  rank: 200
24
- }],
25
- type: 'button'
24
+ }]
26
25
  }];
@@ -115,7 +115,11 @@ export const BlockControlsSurfaceDragHandle = ({
115
115
  return !isTopLevelNode || $anchor.depth > DRAG_HANDLE_MAX_SHIFT_CLICK_DEPTH;
116
116
  }, [api, isLayoutColumn, isShiftDown, isTopLevelNode, view]);
117
117
  const getAnchorName = useCallback(() => {
118
- var _ref, _ref2, _dom$getAttribute, _dom$closest;
118
+ var _api$blockControls$sh2, _api$blockControls$sh3, _ref, _ref2, _dom$getAttribute, _dom$closest;
119
+ const surfaceAnchor = posRef.current === undefined ? undefined : api === null || api === void 0 ? void 0 : (_api$blockControls$sh2 = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh2 === void 0 ? void 0 : (_api$blockControls$sh3 = _api$blockControls$sh2.surfaceAnchors) === null || _api$blockControls$sh3 === void 0 ? void 0 : _api$blockControls$sh3.get(posRef.current);
120
+ if (surfaceAnchor) {
121
+ return surfaceAnchor;
122
+ }
119
123
  if (activeNodeAnchorName) {
120
124
  return activeNodeAnchorName;
121
125
  }
@@ -129,7 +133,7 @@ export const BlockControlsSurfaceDragHandle = ({
129
133
  }
130
134
  const anchorAttrName = getAnchorAttrName();
131
135
  return (_ref = (_ref2 = (_dom$getAttribute = dom.getAttribute(anchorAttrName)) !== null && _dom$getAttribute !== void 0 ? _dom$getAttribute : dom.getAttribute(NODE_ANCHOR_ATTR_NAME)) !== null && _ref2 !== void 0 ? _ref2 : (_dom$closest = dom.closest(`[${anchorAttrName}]`)) === null || _dom$closest === void 0 ? void 0 : _dom$closest.getAttribute(anchorAttrName)) !== null && _ref !== void 0 ? _ref : '';
132
- }, [activeNodeAnchorName, view]);
136
+ }, [activeNodeAnchorName, api, view]);
133
137
  useLayoutEffect(() => {
134
138
  const element = buttonRef.current;
135
139
  if (!view || !element) {
@@ -720,10 +720,12 @@ export const DragHandle = ({
720
720
  });
721
721
  }, [anchorName, api, getPos, nodeType, start, view]);
722
722
  const calculatePositionOld = useCallback(() => {
723
+ var _$pos$nodeAfter;
723
724
  const pos = getPos();
724
- const $pos = expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) ? typeof pos === 'number' && view.state.doc.resolve(pos) : pos && view.state.doc.resolve(pos);
725
+ const $pos = typeof pos === 'number' ? view.state.doc.resolve(pos) : undefined;
725
726
  const parentPos = $pos && $pos.depth ? $pos.before() : undefined;
726
727
  const node = parentPos !== undefined ? view.state.doc.nodeAt(parentPos) : undefined;
728
+ const nodeTypeWithLevel = $pos !== null && $pos !== void 0 && (_$pos$nodeAfter = $pos.nodeAfter) !== null && _$pos$nodeAfter !== void 0 && _$pos$nodeAfter.isBlock ? getNodeTypeWithLevel($pos.nodeAfter) : nodeType;
727
729
  const parentNodeType = node === null || node === void 0 ? void 0 : node.type.name;
728
730
  const supportsAnchor = CSS.supports('top', `anchor(${anchorName} start)`) && CSS.supports('left', `anchor(${anchorName} start)`);
729
731
  const safeAnchorName = editorExperiment('platform_editor_controls', 'variant1') ? refreshAnchorName({
@@ -781,14 +783,14 @@ export const DragHandle = ({
781
783
  const bottom = editorExperiment('platform_editor_controls', 'variant1') ? getControlBottomCSSValue(safeAnchorName, isSticky, isTopLevelNodeValue, isLayoutColumn, ACTIVE_DRAG_HANDLE_FALLBACK_ANCHOR_NAME) : {};
782
784
  return {
783
785
  left: isEdgeCase ? `calc(${anchorStart} + ${getLeftPosition(dom, nodeType, innerContainer, isMacroInteractionUpdates, parentNodeType)})` : editorExperiment('advanced_layouts', true) && isLayoutColumn ? `calc((${anchorRight} + ${anchorLeft})/2 - ${DRAG_HANDLE_HEIGHT / 2}px)` : `calc(${anchorStart} - ${DRAG_HANDLE_WIDTH}px - ${dragHandleGap(nodeType, parentNodeType)}px)`,
784
- top: editorExperiment('advanced_layouts', true) && isLayoutColumn ? `calc(${anchorTop} - ${DRAG_HANDLE_WIDTH}px)` : `calc(${anchorStart} + ${topPositionAdjustment(expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) ? $pos && $pos.nodeAfter && getNodeTypeWithLevel($pos.nodeAfter) || nodeType : nodeType, (dom === null || dom === void 0 ? void 0 : dom.getAttribute('layout')) || '')}px)`,
786
+ top: editorExperiment('advanced_layouts', true) && isLayoutColumn ? `calc(${anchorTop} - ${DRAG_HANDLE_WIDTH}px)` : `calc(${anchorStart} + ${topPositionAdjustment(nodeTypeWithLevel, (dom === null || dom === void 0 ? void 0 : dom.getAttribute('layout')) || '')}px)`,
785
787
  ...bottom
786
788
  };
787
789
  }
788
790
  const height = editorExperiment('platform_editor_controls', 'variant1') ? getControlHeightCSSValue(getNodeHeight(dom, safeAnchorName, anchorRectCache) || 0, isSticky, isTopLevelNodeValue, `${DRAG_HANDLE_HEIGHT}`, isLayoutColumn) : {};
789
791
  return {
790
792
  left: isEdgeCase ? `calc(${(dom === null || dom === void 0 ? void 0 : dom.offsetLeft) || 0}px + ${getLeftPosition(dom, nodeType, innerContainer, isMacroInteractionUpdates, parentNodeType)})` : getLeftPosition(dom, nodeType, innerContainer, isMacroInteractionUpdates, parentNodeType),
791
- top: getTopPosition(dom, nodeType),
793
+ top: getTopPosition(dom, nodeTypeWithLevel),
792
794
  ...height
793
795
  };
794
796
  }, [anchorName, getPos, view, nodeType, blockCardWidth, macroInteractionUpdates, anchorRectCache, isTopLevelNodeValue, isLayoutColumn]);
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
7
7
  // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports
8
8
  import { css, jsx } from '@emotion/react';
9
9
  import { layoutBreakpointWidth } from '@atlaskit/editor-shared-styles';
10
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
10
11
  import { DropIndicator } from '@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box';
11
12
  import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/adapter/element-adapter';
12
13
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
@@ -125,7 +126,7 @@ export const DropTargetLayout = props => {
125
126
  }, isDraggedOver ? jsx(DropIndicator, {
126
127
  edge: "right",
127
128
  gap: `-${DROP_TARGET_LAYOUT_DROP_ZONE_WIDTH}px`
128
- }) : (isActiveAnchor || expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) && jsx("div", {
129
+ }) : (isActiveAnchor || expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) || isExperimentEnabled('platform_editor_block_control_migration')) && jsx("div", {
129
130
  "data-testid": "block-ctrl-drop-hint",
130
131
  css: dropTargetLayoutHintStyle
131
132
  }));
@@ -239,7 +240,7 @@ export const DropTargetLayoutNativeAnchorSupport = props => {
239
240
  }, isDraggedOver ? jsx(DropIndicator, {
240
241
  edge: "right",
241
242
  gap: `-${DROP_TARGET_LAYOUT_DROP_ZONE_WIDTH}px`
242
- }) : (isActiveAnchor || expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) && jsx("div", {
243
+ }) : (isActiveAnchor || expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) || isExperimentEnabled('platform_editor_block_control_migration')) && jsx("div", {
243
244
  "data-testid": "block-ctrl-drop-hint",
244
245
  css: dropTargetLayoutHintStyle
245
246
  }));
@@ -8,6 +8,7 @@ import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
8
8
  import { css, jsx } from '@emotion/react';
9
9
  import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
10
10
  import { getBaseNodeTypeName } from '@atlaskit/editor-common/utils/node-type-utils';
11
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
11
12
  import { DropIndicator } from '@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box';
12
13
  import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/adapter/element-adapter';
13
14
  import { layers } from '@atlaskit/theme/constants';
@@ -85,7 +86,7 @@ const HoverZone = ({
85
86
  const ref = useRef(null);
86
87
  const isRemainingheight = dropTargetStyle === 'remainingHeight';
87
88
  const anchorName = useMemo(() => {
88
- if (expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) {
89
+ if (expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) || isExperimentEnabled('platform_editor_block_control_migration')) {
89
90
  if (node && typeof pos === 'number') {
90
91
  const posOffset = position === 'upper' ? -node.nodeSize : 0;
91
92
  return (api === null || api === void 0 ? void 0 : api.core.actions.getAnchorIdForNode(node, pos + posOffset)) || '';
@@ -136,7 +137,7 @@ const HoverZone = ({
136
137
  // only apply upper drop zone
137
138
  if (isRemainingheight && position === 'upper') {
138
139
  // previous node
139
- const anchorName = node ? expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) ? (api === null || api === void 0 ? void 0 : api.core.actions.getAnchorIdForNode(node, pos || -1)) || '' : getNodeAnchor(node) : '';
140
+ const anchorName = node ? expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true) || isExperimentEnabled('platform_editor_block_control_migration') ? (api === null || api === void 0 ? void 0 : api.core.actions.getAnchorIdForNode(node, pos || -1)) || '' : getNodeAnchor(node) : '';
140
141
  let top = 'unset';
141
142
  if (anchorName) {
142
143
  const enabledDropZone = getEnableDropZone().includes((node === null || node === void 0 ? void 0 : node.type.name) || '');