@atlaskit/editor-plugin-block-menu 15.0.11 → 15.0.13

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.
@@ -1,6 +1,159 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
1
4
  import { getDocument } from '@atlaskit/browser-apis';
2
5
  import { BLOCK_MENU_TEST_ID } from '@atlaskit/editor-common/block-menu';
3
- export var fixBlockMenuPositionAndScroll = function fixBlockMenuPositionAndScroll(firstSelectedNode) {
6
+ import { DRAG_HANDLE_SELECTOR } from '@atlaskit/editor-common/styles';
7
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
8
+ var POPUP_WRAPPER_TEST_ID = 'popup-wrapper';
9
+ var EDITOR_CONTENT_CONTAINER_SELECTOR = '[data-testid="editor-content-container"]';
10
+ /**
11
+ * Breathing room left between the revealed block and the fold it just crossed, so the block does
12
+ * not end up flush against the edge of the viewport.
13
+ */
14
+ var REVEAL_MARGIN = 8;
15
+
16
+ /**
17
+ * Where the drag handle and the popup sit relative to their block. The drag handle is removed from
18
+ * the DOM as soon as the pointer moves onto the block menu, so this has to be measured while the
19
+ * menu opens rather than when a menu item is clicked.
20
+ */
21
+
22
+ /**
23
+ * Minimum scroll needed to bring the drag handle back inside the viewport.
24
+ * Zero means the handle is already fully visible.
25
+ */
26
+ export var getScrollDistanceToRevealAnchor = function getScrollDistanceToRevealAnchor(dragHandleTop, dragHandleHeight, viewport) {
27
+ if (dragHandleTop < viewport.top) {
28
+ return dragHandleTop - viewport.top;
29
+ }
30
+ var dragHandleBottom = dragHandleTop + dragHandleHeight;
31
+ if (dragHandleBottom > viewport.bottom) {
32
+ return dragHandleBottom - viewport.bottom;
33
+ }
34
+ return 0;
35
+ };
36
+
37
+ /**
38
+ * The editor content container is not always the element that actually scrolls - depending on the
39
+ * product it can be an ancestor, or the document itself. Pick the closest one that really scrolls.
40
+ */
41
+ var getScrollContainer = function getScrollContainer(doc, selectedBlock) {
42
+ var editorContentContainer = doc.querySelector(EDITOR_CONTENT_CONTAINER_SELECTOR);
43
+ if (editorContentContainer && editorContentContainer.scrollHeight > editorContentContainer.clientHeight) {
44
+ return editorContentContainer;
45
+ }
46
+ var ancestor = selectedBlock.parentElement;
47
+ while (ancestor) {
48
+ var _doc$defaultView;
49
+ var overflowY = (_doc$defaultView = doc.defaultView) === null || _doc$defaultView === void 0 ? void 0 : _doc$defaultView.getComputedStyle(ancestor).overflowY;
50
+ if ((overflowY === 'auto' || overflowY === 'scroll') && ancestor.scrollHeight > ancestor.clientHeight) {
51
+ return ancestor;
52
+ }
53
+ ancestor = ancestor.parentElement;
54
+ }
55
+ return doc.scrollingElement;
56
+ };
57
+ var getScrollViewport = function getScrollViewport(doc, scrollContainer) {
58
+ if (scrollContainer === doc.scrollingElement) {
59
+ var _doc$defaultView$inne, _doc$defaultView2;
60
+ return {
61
+ bottom: (_doc$defaultView$inne = (_doc$defaultView2 = doc.defaultView) === null || _doc$defaultView2 === void 0 ? void 0 : _doc$defaultView2.innerHeight) !== null && _doc$defaultView$inne !== void 0 ? _doc$defaultView$inne : 0,
62
+ top: 0
63
+ };
64
+ }
65
+ var _scrollContainer$getB = scrollContainer.getBoundingClientRect(),
66
+ top = _scrollContainer$getB.top,
67
+ bottom = _scrollContainer$getB.bottom;
68
+ return {
69
+ bottom: bottom,
70
+ top: top
71
+ };
72
+ };
73
+
74
+ /**
75
+ * Float based node views - a media group is one - collapse their top level wrapper to zero height,
76
+ * so the visual bottom of the block has to come from its children.
77
+ */
78
+ var getBlockBottom = function getBlockBottom(block) {
79
+ var rect = block.getBoundingClientRect();
80
+ if (rect.height > 0) {
81
+ return rect.bottom;
82
+ }
83
+ return Array.from(block.children).reduce(function (bottom, child) {
84
+ return Math.max(bottom, child.getBoundingClientRect().bottom);
85
+ }, rect.bottom);
86
+ };
87
+ var setPopupTop = function setPopupTop(blockMenuEl, targetTop) {
88
+ var _blockMenuEl$closest;
89
+ var popupWrapper = (_blockMenuEl$closest = blockMenuEl.closest("[data-testid=\"".concat(POPUP_WRAPPER_TEST_ID, "\"]"))) !== null && _blockMenuEl$closest !== void 0 ? _blockMenuEl$closest : blockMenuEl.parentElement;
90
+ if (!(popupWrapper instanceof HTMLElement)) {
91
+ return;
92
+ }
93
+ var distance = blockMenuEl.getBoundingClientRect().top - targetTop;
94
+ var hasTopProperty = popupWrapper.style.top !== '';
95
+ var hasBottomProperty = popupWrapper.style.bottom !== '';
96
+ if (hasBottomProperty && !hasTopProperty) {
97
+ popupWrapper.style.bottom = "".concat(parseFloat(popupWrapper.style.bottom || '0') + distance, "px");
98
+ } else {
99
+ popupWrapper.style.top = "".concat(parseFloat(popupWrapper.style.top || '0') - distance, "px");
100
+ }
101
+ };
102
+
103
+ /**
104
+ * Measured while the menu opens, when the popup is still aligned to the drag handle. Moves that
105
+ * keep the handle visible deliberately leave the popup where it is, so this alignment - not the
106
+ * drifted one - is what the popup is restored to when a fold is crossed.
107
+ */
108
+ export var getBlockMenuAnchorMetrics = function getBlockMenuAnchorMetrics(selectedBlock) {
109
+ var _doc$querySelector;
110
+ var doc = getDocument();
111
+ var blockMenuEl = doc === null || doc === void 0 ? void 0 : doc.querySelector("[data-testid=\"".concat(BLOCK_MENU_TEST_ID, "\"]"));
112
+ if (!doc || !selectedBlock || !blockMenuEl) {
113
+ return;
114
+ }
115
+ var blockMenuRect = blockMenuEl.getBoundingClientRect();
116
+ var dragHandleRect = (_doc$querySelector = doc.querySelector(DRAG_HANDLE_SELECTOR)) === null || _doc$querySelector === void 0 ? void 0 : _doc$querySelector.getBoundingClientRect();
117
+ if (blockMenuRect.height === 0 || !(dragHandleRect !== null && dragHandleRect !== void 0 && dragHandleRect.height)) {
118
+ return;
119
+ }
120
+ var blockTop = selectedBlock.getBoundingClientRect().top;
121
+ return {
122
+ blockHeight: getBlockBottom(selectedBlock) - blockTop,
123
+ dragHandleHeight: dragHandleRect.height,
124
+ dragHandleOffsetFromBlock: dragHandleRect.top - blockTop,
125
+ popupOffsetFromBlock: blockMenuRect.top - blockTop,
126
+ popupHeight: blockMenuRect.height
127
+ };
128
+ };
129
+
130
+ /**
131
+ * Captured before the move transaction, so the popup can be realigned to the block after it.
132
+ */
133
+ export var getBlockMenuPositionSnapshot = function getBlockMenuPositionSnapshot(selectedBlock, anchorMetrics) {
134
+ var doc = getDocument();
135
+ if (!doc || !selectedBlock) {
136
+ return;
137
+ }
138
+ var scrollContainer = getScrollContainer(doc, selectedBlock);
139
+ var metrics = anchorMetrics !== null && anchorMetrics !== void 0 ? anchorMetrics : getBlockMenuAnchorMetrics(selectedBlock);
140
+ if (!scrollContainer || !metrics) {
141
+ return;
142
+ }
143
+ return _objectSpread(_objectSpread({}, metrics), {}, {
144
+ scrollContainer: scrollContainer,
145
+ scrollTop: scrollContainer.scrollTop
146
+ });
147
+ };
148
+
149
+ /**
150
+ * Called after a move up/down transaction.
151
+ *
152
+ * With a `positionSnapshot` the popup is only sticky when it has to be: the editor stays put while
153
+ * the moved node's drag handle is still visible, and scrolls the minimum needed to reveal the
154
+ * handle otherwise - realigning the popup to the revealed handle.
155
+ */
156
+ export var fixBlockMenuPositionAndScroll = function fixBlockMenuPositionAndScroll(selectedNode, positionSnapshot) {
4
157
  var doc = getDocument();
5
158
  if (!doc) {
6
159
  return;
@@ -9,13 +162,54 @@ export var fixBlockMenuPositionAndScroll = function fixBlockMenuPositionAndScrol
9
162
  if (!(blockMenuEl !== null && blockMenuEl !== void 0 && blockMenuEl.parentElement)) {
10
163
  return;
11
164
  }
12
- var scrollableContainer = doc.querySelector('[data-testid="editor-content-container"]');
13
- if (!firstSelectedNode || !scrollableContainer) {
165
+ if (positionSnapshot) {
166
+ if (!selectedNode) {
167
+ return;
168
+ }
169
+ // Reuse the container resolved when the snapshot was taken - re-resolving it independently
170
+ // here could pick a different element (e.g. for a node near an overflow boundary), applying
171
+ // positionSnapshot.scrollTop to the wrong container.
172
+ var scrollContainer = positionSnapshot.scrollContainer;
173
+
174
+ // The move transaction scrolls the new selection into view - undo that, only a drag handle
175
+ // leaving the viewport is allowed to move the editor.
176
+ scrollContainer.scrollTo({
177
+ behavior: 'instant',
178
+ top: positionSnapshot.scrollTop
179
+ });
180
+ var viewport = getScrollViewport(doc, scrollContainer);
181
+ var anchorTop = selectedNode.getBoundingClientRect().top + positionSnapshot.dragHandleOffsetFromBlock;
182
+
183
+ // Whether to scroll at all is decided by the drag handle alone - a block taller than the
184
+ // viewport is expected to be cut off while its handle stays usable.
185
+ if (getScrollDistanceToRevealAnchor(anchorTop, positionSnapshot.dragHandleHeight, viewport) === 0) {
186
+ return;
187
+ }
188
+
189
+ // Once we do scroll, reveal the block and the popup, not just the handle sized sliver of it.
190
+ // popupOffsetFromBlock is relative to the block - re-express it relative to the handle (the
191
+ // anchor everything else here is measured from) to build one span covering all three. The
192
+ // handle itself (0 to dragHandleHeight) always stays inside that span, so its visibility is
193
+ // never sacrificed to make room for the block or the popup.
194
+ var popupTopFromHandle = positionSnapshot.popupOffsetFromBlock - positionSnapshot.dragHandleOffsetFromBlock;
195
+ var revealTop = Math.min(0, popupTopFromHandle);
196
+ var revealBottom = Math.max(positionSnapshot.dragHandleHeight, positionSnapshot.blockHeight - positionSnapshot.dragHandleOffsetFromBlock, popupTopFromHandle + positionSnapshot.popupHeight);
197
+ var revealHeight = Math.min(revealBottom - revealTop, Math.max(viewport.bottom - viewport.top - 2 * REVEAL_MARGIN, 0));
198
+ var scrollDistance = getScrollDistanceToRevealAnchor(anchorTop + revealTop - REVEAL_MARGIN, revealHeight + 2 * REVEAL_MARGIN, viewport);
199
+ scrollContainer.scrollBy({
200
+ behavior: 'instant',
201
+ top: scrollDistance
202
+ });
203
+ setPopupTop(blockMenuEl, selectedNode.getBoundingClientRect().top + positionSnapshot.popupOffsetFromBlock);
204
+ return;
205
+ }
206
+ var scrollableContainer = doc.querySelector(EDITOR_CONTENT_CONTAINER_SELECTOR);
207
+ if (!selectedNode || !scrollableContainer) {
14
208
  return;
15
209
  }
16
210
  var parentElement = blockMenuEl.parentElement;
17
211
  var currentTop = parentElement.getBoundingClientRect().top;
18
- var distance = firstSelectedNode.getBoundingClientRect().top - blockMenuEl.getBoundingClientRect().top;
212
+ var distance = selectedNode.getBoundingClientRect().top - blockMenuEl.getBoundingClientRect().top;
19
213
  scrollableContainer.scrollBy({
20
214
  behavior: 'instant',
21
215
  top: distance
@@ -31,4 +225,22 @@ export var fixBlockMenuPositionAndScroll = function fixBlockMenuPositionAndScrol
31
225
  var currentTopValue = parseFloat(parentElement.style.top || '0');
32
226
  parentElement.style.top = "".concat(currentTopValue + topDifference, "px");
33
227
  }
228
+ };
229
+
230
+ /**
231
+ * Shared by move-up.tsx and move-down.tsx: schedules the post-move fix for the frame after
232
+ * ProseMirror has re-rendered.
233
+ *
234
+ * Falls back to the pre-PR always-scroll path (matching gate-off, via `getFirstSelectedDomNode`)
235
+ * whenever a `positionSnapshot` wasn't captured - e.g. a media/file thumbnail reporting zero height
236
+ * for a frame when the menu opened - rather than silently doing nothing.
237
+ */
238
+ export var scheduleBlockMenuPositionFix = function scheduleBlockMenuPositionFix(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode) {
239
+ requestAnimationFrame(function () {
240
+ if (fg('platform_editor_blocks_patch_8') && positionSnapshot) {
241
+ fixBlockMenuPositionAndScroll(getMovedBlockDomNode(), positionSnapshot);
242
+ return;
243
+ }
244
+ fixBlockMenuPositionAndScroll(getFirstSelectedDomNode());
245
+ });
34
246
  };
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
3
3
  import type { EditorView } from '@atlaskit/editor-prosemirror/view';
4
4
  import type { BlockMenuPlugin } from '../blockMenuPluginType';
5
+ import { type BlockMenuAnchorMetrics } from './utils/fixBlockMenuPositionAndScroll';
5
6
  export type Direction = 'moveUp' | 'moveDown';
6
7
  type BlockMenuProviderProps = {
7
8
  api: ExtractInjectionAPI<BlockMenuPlugin> | undefined;
@@ -9,7 +10,19 @@ type BlockMenuProviderProps = {
9
10
  editorView: EditorView | undefined;
10
11
  };
11
12
  export type BlockMenuContextType = {
13
+ /**
14
+ * Drag handle and popup geometry as laid out when the menu opened.
15
+ */
16
+ anchorMetricsRef: React.MutableRefObject<BlockMenuAnchorMetrics | undefined>;
12
17
  getFirstSelectedDomNode: () => Element | undefined;
18
+ /**
19
+ * The top level block a move transaction has just moved.
20
+ */
21
+ getMovedBlockDomNode: () => Element | undefined;
22
+ /**
23
+ * The top level block the block menu is open for, which is what the drag handle is anchored to.
24
+ */
25
+ getSelectedBlockDomNode: () => Element | undefined;
13
26
  moveDownRef: React.MutableRefObject<HTMLButtonElement | null>;
14
27
  moveUpRef: React.MutableRefObject<HTMLButtonElement | null>;
15
28
  /**
@@ -1 +1,55 @@
1
- export declare const fixBlockMenuPositionAndScroll: (firstSelectedNode: Element | undefined) => void;
1
+ /**
2
+ * Where the drag handle and the popup sit relative to their block. The drag handle is removed from
3
+ * the DOM as soon as the pointer moves onto the block menu, so this has to be measured while the
4
+ * menu opens rather than when a menu item is clicked.
5
+ */
6
+ export type BlockMenuAnchorMetrics = {
7
+ /**
8
+ * Moving a node can recreate its DOM, and node views that render asynchronously (media) report
9
+ * zero height for a frame or two afterwards - so the height is taken from before the move.
10
+ */
11
+ blockHeight: number;
12
+ dragHandleHeight: number;
13
+ dragHandleOffsetFromBlock: number;
14
+ popupHeight: number;
15
+ popupOffsetFromBlock: number;
16
+ };
17
+ export type BlockMenuPositionSnapshot = BlockMenuAnchorMetrics & {
18
+ scrollContainer: Element;
19
+ scrollTop: number;
20
+ };
21
+ /**
22
+ * Minimum scroll needed to bring the drag handle back inside the viewport.
23
+ * Zero means the handle is already fully visible.
24
+ */
25
+ export declare const getScrollDistanceToRevealAnchor: (dragHandleTop: number, dragHandleHeight: number, viewport: {
26
+ bottom: number;
27
+ top: number;
28
+ }) => number;
29
+ /**
30
+ * Measured while the menu opens, when the popup is still aligned to the drag handle. Moves that
31
+ * keep the handle visible deliberately leave the popup where it is, so this alignment - not the
32
+ * drifted one - is what the popup is restored to when a fold is crossed.
33
+ */
34
+ export declare const getBlockMenuAnchorMetrics: (selectedBlock: Element | undefined) => BlockMenuAnchorMetrics | undefined;
35
+ /**
36
+ * Captured before the move transaction, so the popup can be realigned to the block after it.
37
+ */
38
+ export declare const getBlockMenuPositionSnapshot: (selectedBlock: Element | undefined, anchorMetrics: BlockMenuAnchorMetrics | undefined) => BlockMenuPositionSnapshot | undefined;
39
+ /**
40
+ * Called after a move up/down transaction.
41
+ *
42
+ * With a `positionSnapshot` the popup is only sticky when it has to be: the editor stays put while
43
+ * the moved node's drag handle is still visible, and scrolls the minimum needed to reveal the
44
+ * handle otherwise - realigning the popup to the revealed handle.
45
+ */
46
+ export declare const fixBlockMenuPositionAndScroll: (selectedNode: Element | undefined, positionSnapshot?: BlockMenuPositionSnapshot) => void;
47
+ /**
48
+ * Shared by move-up.tsx and move-down.tsx: schedules the post-move fix for the frame after
49
+ * ProseMirror has re-rendered.
50
+ *
51
+ * Falls back to the pre-PR always-scroll path (matching gate-off, via `getFirstSelectedDomNode`)
52
+ * whenever a `positionSnapshot` wasn't captured - e.g. a media/file thumbnail reporting zero height
53
+ * for a frame when the menu opened - rather than silently doing nothing.
54
+ */
55
+ export declare const scheduleBlockMenuPositionFix: (positionSnapshot: BlockMenuPositionSnapshot | undefined, getMovedBlockDomNode: () => Element | undefined, getFirstSelectedDomNode: () => Element | undefined) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-block-menu",
3
- "version": "15.0.11",
3
+ "version": "15.0.13",
4
4
  "description": "BlockMenu plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -37,7 +37,7 @@
37
37
  "@atlaskit/platform-feature-flags": "^2.2.0",
38
38
  "@atlaskit/primitives": "^22.5.0",
39
39
  "@atlaskit/prosemirror-history": "^1.2.0",
40
- "@atlaskit/tmp-editor-statsig": "^170.0.0",
40
+ "@atlaskit/tmp-editor-statsig": "^171.0.0",
41
41
  "@atlaskit/tokens": "^16.11.0",
42
42
  "@babel/runtime": "^7.0.0",
43
43
  "bind-event-listener": "^3.0.0",
@@ -94,6 +94,10 @@
94
94
  "platform_editor_block_menu_jira_patch_5": {
95
95
  "type": "boolean"
96
96
  },
97
+ "platform_editor_blocks_patch_8": {
98
+ "type": "boolean",
99
+ "referenceOnly": true
100
+ },
97
101
  "cc-maui-phase-3": {
98
102
  "type": "boolean"
99
103
  }