@atlaskit/editor-plugin-block-menu 15.0.10 → 15.0.12
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 +17 -0
- package/dist/cjs/ui/block-menu-provider.js +71 -10
- package/dist/cjs/ui/block-menu.js +22 -1
- package/dist/cjs/ui/move-down.js +7 -5
- package/dist/cjs/ui/move-up.js +7 -5
- package/dist/cjs/ui/utils/fixBlockMenuPositionAndScroll.js +218 -5
- package/dist/es2019/ui/block-menu-provider.js +67 -10
- package/dist/es2019/ui/block-menu.js +20 -1
- package/dist/es2019/ui/move-down.js +8 -6
- package/dist/es2019/ui/move-up.js +8 -6
- package/dist/es2019/ui/utils/fixBlockMenuPositionAndScroll.js +213 -4
- package/dist/esm/ui/block-menu-provider.js +71 -10
- package/dist/esm/ui/block-menu.js +22 -1
- package/dist/esm/ui/move-down.js +8 -6
- package/dist/esm/ui/move-up.js +8 -6
- package/dist/esm/ui/utils/fixBlockMenuPositionAndScroll.js +216 -4
- package/dist/types/ui/block-menu-provider.d.ts +13 -0
- package/dist/types/ui/utils/fixBlockMenuPositionAndScroll.d.ts +55 -1
- package/package.json +7 -3
|
@@ -249,8 +249,9 @@ const BlockMenu = ({
|
|
|
249
249
|
if (!isMenuOpen) {
|
|
250
250
|
return;
|
|
251
251
|
}
|
|
252
|
+
onDropdownOpenChanged(true);
|
|
252
253
|
setMenuHeight(((_popupRef$current = popupRef.current) === null || _popupRef$current === void 0 ? void 0 : _popupRef$current.clientHeight) || FALLBACK_MENU_HEIGHT);
|
|
253
|
-
}, [isMenuOpen]);
|
|
254
|
+
}, [isMenuOpen, onDropdownOpenChanged]);
|
|
254
255
|
const hasFocus = (_ref2 = (editorView === null || editorView === void 0 ? void 0 : editorView.hasFocus()) ||
|
|
255
256
|
// eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
|
|
256
257
|
document.activeElement === targetHandleRef || popupRef.current && (
|
|
@@ -339,6 +340,24 @@ const BlockMenu = ({
|
|
|
339
340
|
stick: true
|
|
340
341
|
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
|
|
341
342
|
,
|
|
343
|
+
onPositionCalculated: fg('platform_editor_blocks_patch_8') ? position => {
|
|
344
|
+
if (position.left === undefined) {
|
|
345
|
+
// Already placed on the gutter side (using `right`) - nothing to correct.
|
|
346
|
+
return position;
|
|
347
|
+
}
|
|
348
|
+
// Popup prefers whichever side has more room, which flips the menu onto the
|
|
349
|
+
// content side when the gutter is too narrow to fit it. Force it back onto the
|
|
350
|
+
// gutter side instead - it can still overlap the content once there, but it
|
|
351
|
+
// must not start on top of the node. `position.left` is shifted by the same
|
|
352
|
+
// distance regardless of the offset parent/scroll it was calculated against,
|
|
353
|
+
// since that just moves both by the same amount.
|
|
354
|
+
return {
|
|
355
|
+
...position,
|
|
356
|
+
left: position.left - DEFAULT_MENU_WIDTH - DRAG_HANDLE_WIDTH - 2 * DRAG_HANDLE_OFFSET_PADDING
|
|
357
|
+
};
|
|
358
|
+
} : undefined
|
|
359
|
+
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
|
|
360
|
+
,
|
|
342
361
|
offset: [DRAG_HANDLE_WIDTH + DRAG_HANDLE_OFFSET_PADDING, targetHandleHeightOffset],
|
|
343
362
|
focusTrap: openedViaKeyboard ?
|
|
344
363
|
// Only enable focus trap when opened via keyboard to make sure the focus is on the first focusable menu item
|
|
@@ -8,9 +8,10 @@ import { blockMenuMessages as messages } from '@atlaskit/editor-common/messages'
|
|
|
8
8
|
import { DIRECTION } from '@atlaskit/editor-common/types';
|
|
9
9
|
import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
|
|
10
10
|
import ArrowDownIcon from '@atlaskit/icon/core/arrow-down';
|
|
11
|
+
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
11
12
|
import { useBlockMenu } from './block-menu-provider';
|
|
12
13
|
import { BLOCK_MENU_ITEM_NAME } from './consts';
|
|
13
|
-
import {
|
|
14
|
+
import { getBlockMenuPositionSnapshot, scheduleBlockMenuPositionFix } from './utils/fixBlockMenuPositionAndScroll';
|
|
14
15
|
const MoveDownDropdownItemContent = ({
|
|
15
16
|
api
|
|
16
17
|
}) => {
|
|
@@ -20,7 +21,10 @@ const MoveDownDropdownItemContent = ({
|
|
|
20
21
|
const {
|
|
21
22
|
moveUpRef,
|
|
22
23
|
moveDownRef,
|
|
23
|
-
getFirstSelectedDomNode
|
|
24
|
+
getFirstSelectedDomNode,
|
|
25
|
+
getMovedBlockDomNode,
|
|
26
|
+
getSelectedBlockDomNode,
|
|
27
|
+
anchorMetricsRef
|
|
24
28
|
} = useBlockMenu();
|
|
25
29
|
const {
|
|
26
30
|
canMoveDown
|
|
@@ -41,6 +45,7 @@ const MoveDownDropdownItemContent = ({
|
|
|
41
45
|
}
|
|
42
46
|
}, [canMoveDown, moveUpRef, moveDownRef]);
|
|
43
47
|
const handleClick = () => {
|
|
48
|
+
const positionSnapshot = fg('platform_editor_blocks_patch_8') ? getBlockMenuPositionSnapshot(getSelectedBlockDomNode(), anchorMetricsRef.current) : undefined;
|
|
44
49
|
api === null || api === void 0 ? void 0 : api.core.actions.execute(({
|
|
45
50
|
tr
|
|
46
51
|
}) => {
|
|
@@ -59,10 +64,7 @@ const MoveDownDropdownItemContent = ({
|
|
|
59
64
|
});
|
|
60
65
|
return tr;
|
|
61
66
|
});
|
|
62
|
-
|
|
63
|
-
const newFirstNode = getFirstSelectedDomNode();
|
|
64
|
-
fixBlockMenuPositionAndScroll(newFirstNode);
|
|
65
|
-
});
|
|
67
|
+
scheduleBlockMenuPositionFix(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode);
|
|
66
68
|
};
|
|
67
69
|
return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
|
|
68
70
|
triggerRef: moveDownRef,
|
|
@@ -8,9 +8,10 @@ import { blockMenuMessages as messages } from '@atlaskit/editor-common/messages'
|
|
|
8
8
|
import { DIRECTION } from '@atlaskit/editor-common/types';
|
|
9
9
|
import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
|
|
10
10
|
import ArrowUpIcon from '@atlaskit/icon/core/arrow-up';
|
|
11
|
+
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
11
12
|
import { useBlockMenu } from './block-menu-provider';
|
|
12
13
|
import { BLOCK_MENU_ITEM_NAME } from './consts';
|
|
13
|
-
import {
|
|
14
|
+
import { getBlockMenuPositionSnapshot, scheduleBlockMenuPositionFix } from './utils/fixBlockMenuPositionAndScroll';
|
|
14
15
|
const MoveUpDropdownItemContent = ({
|
|
15
16
|
api
|
|
16
17
|
}) => {
|
|
@@ -20,7 +21,10 @@ const MoveUpDropdownItemContent = ({
|
|
|
20
21
|
const {
|
|
21
22
|
moveUpRef,
|
|
22
23
|
moveDownRef,
|
|
23
|
-
getFirstSelectedDomNode
|
|
24
|
+
getFirstSelectedDomNode,
|
|
25
|
+
getMovedBlockDomNode,
|
|
26
|
+
getSelectedBlockDomNode,
|
|
27
|
+
anchorMetricsRef
|
|
24
28
|
} = useBlockMenu();
|
|
25
29
|
const {
|
|
26
30
|
canMoveUp
|
|
@@ -39,6 +43,7 @@ const MoveUpDropdownItemContent = ({
|
|
|
39
43
|
}
|
|
40
44
|
}, [canMoveUp, moveDownRef, moveUpRef]);
|
|
41
45
|
const handleClick = () => {
|
|
46
|
+
const positionSnapshot = fg('platform_editor_blocks_patch_8') ? getBlockMenuPositionSnapshot(getSelectedBlockDomNode(), anchorMetricsRef.current) : undefined;
|
|
42
47
|
api === null || api === void 0 ? void 0 : api.core.actions.execute(({
|
|
43
48
|
tr
|
|
44
49
|
}) => {
|
|
@@ -57,10 +62,7 @@ const MoveUpDropdownItemContent = ({
|
|
|
57
62
|
});
|
|
58
63
|
return tr;
|
|
59
64
|
});
|
|
60
|
-
|
|
61
|
-
const newFirstNode = getFirstSelectedDomNode();
|
|
62
|
-
fixBlockMenuPositionAndScroll(newFirstNode);
|
|
63
|
-
});
|
|
65
|
+
scheduleBlockMenuPositionFix(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode);
|
|
64
66
|
};
|
|
65
67
|
return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
|
|
66
68
|
triggerRef: moveUpRef,
|
|
@@ -1,6 +1,156 @@
|
|
|
1
1
|
import { getDocument } from '@atlaskit/browser-apis';
|
|
2
2
|
import { BLOCK_MENU_TEST_ID } from '@atlaskit/editor-common/block-menu';
|
|
3
|
-
|
|
3
|
+
import { DRAG_HANDLE_SELECTOR } from '@atlaskit/editor-common/styles';
|
|
4
|
+
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
5
|
+
const POPUP_WRAPPER_TEST_ID = 'popup-wrapper';
|
|
6
|
+
const EDITOR_CONTENT_CONTAINER_SELECTOR = '[data-testid="editor-content-container"]';
|
|
7
|
+
/**
|
|
8
|
+
* Breathing room left between the revealed block and the fold it just crossed, so the block does
|
|
9
|
+
* not end up flush against the edge of the viewport.
|
|
10
|
+
*/
|
|
11
|
+
const REVEAL_MARGIN = 8;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Where the drag handle and the popup sit relative to their block. The drag handle is removed from
|
|
15
|
+
* the DOM as soon as the pointer moves onto the block menu, so this has to be measured while the
|
|
16
|
+
* menu opens rather than when a menu item is clicked.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Minimum scroll needed to bring the drag handle back inside the viewport.
|
|
21
|
+
* Zero means the handle is already fully visible.
|
|
22
|
+
*/
|
|
23
|
+
export const getScrollDistanceToRevealAnchor = (dragHandleTop, dragHandleHeight, viewport) => {
|
|
24
|
+
if (dragHandleTop < viewport.top) {
|
|
25
|
+
return dragHandleTop - viewport.top;
|
|
26
|
+
}
|
|
27
|
+
const dragHandleBottom = dragHandleTop + dragHandleHeight;
|
|
28
|
+
if (dragHandleBottom > viewport.bottom) {
|
|
29
|
+
return dragHandleBottom - viewport.bottom;
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The editor content container is not always the element that actually scrolls - depending on the
|
|
36
|
+
* product it can be an ancestor, or the document itself. Pick the closest one that really scrolls.
|
|
37
|
+
*/
|
|
38
|
+
const getScrollContainer = (doc, selectedBlock) => {
|
|
39
|
+
const editorContentContainer = doc.querySelector(EDITOR_CONTENT_CONTAINER_SELECTOR);
|
|
40
|
+
if (editorContentContainer && editorContentContainer.scrollHeight > editorContentContainer.clientHeight) {
|
|
41
|
+
return editorContentContainer;
|
|
42
|
+
}
|
|
43
|
+
let ancestor = selectedBlock.parentElement;
|
|
44
|
+
while (ancestor) {
|
|
45
|
+
var _doc$defaultView;
|
|
46
|
+
const overflowY = (_doc$defaultView = doc.defaultView) === null || _doc$defaultView === void 0 ? void 0 : _doc$defaultView.getComputedStyle(ancestor).overflowY;
|
|
47
|
+
if ((overflowY === 'auto' || overflowY === 'scroll') && ancestor.scrollHeight > ancestor.clientHeight) {
|
|
48
|
+
return ancestor;
|
|
49
|
+
}
|
|
50
|
+
ancestor = ancestor.parentElement;
|
|
51
|
+
}
|
|
52
|
+
return doc.scrollingElement;
|
|
53
|
+
};
|
|
54
|
+
const getScrollViewport = (doc, scrollContainer) => {
|
|
55
|
+
if (scrollContainer === doc.scrollingElement) {
|
|
56
|
+
var _doc$defaultView$inne, _doc$defaultView2;
|
|
57
|
+
return {
|
|
58
|
+
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,
|
|
59
|
+
top: 0
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const {
|
|
63
|
+
top,
|
|
64
|
+
bottom
|
|
65
|
+
} = scrollContainer.getBoundingClientRect();
|
|
66
|
+
return {
|
|
67
|
+
bottom,
|
|
68
|
+
top
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Float based node views - a media group is one - collapse their top level wrapper to zero height,
|
|
74
|
+
* so the visual bottom of the block has to come from its children.
|
|
75
|
+
*/
|
|
76
|
+
const getBlockBottom = block => {
|
|
77
|
+
const rect = block.getBoundingClientRect();
|
|
78
|
+
if (rect.height > 0) {
|
|
79
|
+
return rect.bottom;
|
|
80
|
+
}
|
|
81
|
+
return Array.from(block.children).reduce((bottom, child) => Math.max(bottom, child.getBoundingClientRect().bottom), rect.bottom);
|
|
82
|
+
};
|
|
83
|
+
const setPopupTop = (blockMenuEl, targetTop) => {
|
|
84
|
+
var _blockMenuEl$closest;
|
|
85
|
+
const popupWrapper = (_blockMenuEl$closest = blockMenuEl.closest(`[data-testid="${POPUP_WRAPPER_TEST_ID}"]`)) !== null && _blockMenuEl$closest !== void 0 ? _blockMenuEl$closest : blockMenuEl.parentElement;
|
|
86
|
+
if (!(popupWrapper instanceof HTMLElement)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const distance = blockMenuEl.getBoundingClientRect().top - targetTop;
|
|
90
|
+
const hasTopProperty = popupWrapper.style.top !== '';
|
|
91
|
+
const hasBottomProperty = popupWrapper.style.bottom !== '';
|
|
92
|
+
if (hasBottomProperty && !hasTopProperty) {
|
|
93
|
+
popupWrapper.style.bottom = `${parseFloat(popupWrapper.style.bottom || '0') + distance}px`;
|
|
94
|
+
} else {
|
|
95
|
+
popupWrapper.style.top = `${parseFloat(popupWrapper.style.top || '0') - distance}px`;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Measured while the menu opens, when the popup is still aligned to the drag handle. Moves that
|
|
101
|
+
* keep the handle visible deliberately leave the popup where it is, so this alignment - not the
|
|
102
|
+
* drifted one - is what the popup is restored to when a fold is crossed.
|
|
103
|
+
*/
|
|
104
|
+
export const getBlockMenuAnchorMetrics = selectedBlock => {
|
|
105
|
+
var _doc$querySelector;
|
|
106
|
+
const doc = getDocument();
|
|
107
|
+
const blockMenuEl = doc === null || doc === void 0 ? void 0 : doc.querySelector(`[data-testid="${BLOCK_MENU_TEST_ID}"]`);
|
|
108
|
+
if (!doc || !selectedBlock || !blockMenuEl) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const blockMenuRect = blockMenuEl.getBoundingClientRect();
|
|
112
|
+
const dragHandleRect = (_doc$querySelector = doc.querySelector(DRAG_HANDLE_SELECTOR)) === null || _doc$querySelector === void 0 ? void 0 : _doc$querySelector.getBoundingClientRect();
|
|
113
|
+
if (blockMenuRect.height === 0 || !(dragHandleRect !== null && dragHandleRect !== void 0 && dragHandleRect.height)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const blockTop = selectedBlock.getBoundingClientRect().top;
|
|
117
|
+
return {
|
|
118
|
+
blockHeight: getBlockBottom(selectedBlock) - blockTop,
|
|
119
|
+
dragHandleHeight: dragHandleRect.height,
|
|
120
|
+
dragHandleOffsetFromBlock: dragHandleRect.top - blockTop,
|
|
121
|
+
popupOffsetFromBlock: blockMenuRect.top - blockTop,
|
|
122
|
+
popupHeight: blockMenuRect.height
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Captured before the move transaction, so the popup can be realigned to the block after it.
|
|
128
|
+
*/
|
|
129
|
+
export const getBlockMenuPositionSnapshot = (selectedBlock, anchorMetrics) => {
|
|
130
|
+
const doc = getDocument();
|
|
131
|
+
if (!doc || !selectedBlock) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const scrollContainer = getScrollContainer(doc, selectedBlock);
|
|
135
|
+
const metrics = anchorMetrics !== null && anchorMetrics !== void 0 ? anchorMetrics : getBlockMenuAnchorMetrics(selectedBlock);
|
|
136
|
+
if (!scrollContainer || !metrics) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
...metrics,
|
|
141
|
+
scrollContainer,
|
|
142
|
+
scrollTop: scrollContainer.scrollTop
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Called after a move up/down transaction.
|
|
148
|
+
*
|
|
149
|
+
* With a `positionSnapshot` the popup is only sticky when it has to be: the editor stays put while
|
|
150
|
+
* the moved node's drag handle is still visible, and scrolls the minimum needed to reveal the
|
|
151
|
+
* handle otherwise - realigning the popup to the revealed handle.
|
|
152
|
+
*/
|
|
153
|
+
export const fixBlockMenuPositionAndScroll = (selectedNode, positionSnapshot) => {
|
|
4
154
|
const doc = getDocument();
|
|
5
155
|
if (!doc) {
|
|
6
156
|
return;
|
|
@@ -9,13 +159,54 @@ export const fixBlockMenuPositionAndScroll = firstSelectedNode => {
|
|
|
9
159
|
if (!(blockMenuEl !== null && blockMenuEl !== void 0 && blockMenuEl.parentElement)) {
|
|
10
160
|
return;
|
|
11
161
|
}
|
|
12
|
-
|
|
13
|
-
|
|
162
|
+
if (positionSnapshot) {
|
|
163
|
+
if (!selectedNode) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Reuse the container resolved when the snapshot was taken - re-resolving it independently
|
|
167
|
+
// here could pick a different element (e.g. for a node near an overflow boundary), applying
|
|
168
|
+
// positionSnapshot.scrollTop to the wrong container.
|
|
169
|
+
const scrollContainer = positionSnapshot.scrollContainer;
|
|
170
|
+
|
|
171
|
+
// The move transaction scrolls the new selection into view - undo that, only a drag handle
|
|
172
|
+
// leaving the viewport is allowed to move the editor.
|
|
173
|
+
scrollContainer.scrollTo({
|
|
174
|
+
behavior: 'instant',
|
|
175
|
+
top: positionSnapshot.scrollTop
|
|
176
|
+
});
|
|
177
|
+
const viewport = getScrollViewport(doc, scrollContainer);
|
|
178
|
+
const anchorTop = selectedNode.getBoundingClientRect().top + positionSnapshot.dragHandleOffsetFromBlock;
|
|
179
|
+
|
|
180
|
+
// Whether to scroll at all is decided by the drag handle alone - a block taller than the
|
|
181
|
+
// viewport is expected to be cut off while its handle stays usable.
|
|
182
|
+
if (getScrollDistanceToRevealAnchor(anchorTop, positionSnapshot.dragHandleHeight, viewport) === 0) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Once we do scroll, reveal the block and the popup, not just the handle sized sliver of it.
|
|
187
|
+
// popupOffsetFromBlock is relative to the block - re-express it relative to the handle (the
|
|
188
|
+
// anchor everything else here is measured from) to build one span covering all three. The
|
|
189
|
+
// handle itself (0 to dragHandleHeight) always stays inside that span, so its visibility is
|
|
190
|
+
// never sacrificed to make room for the block or the popup.
|
|
191
|
+
const popupTopFromHandle = positionSnapshot.popupOffsetFromBlock - positionSnapshot.dragHandleOffsetFromBlock;
|
|
192
|
+
const revealTop = Math.min(0, popupTopFromHandle);
|
|
193
|
+
const revealBottom = Math.max(positionSnapshot.dragHandleHeight, positionSnapshot.blockHeight - positionSnapshot.dragHandleOffsetFromBlock, popupTopFromHandle + positionSnapshot.popupHeight);
|
|
194
|
+
const revealHeight = Math.min(revealBottom - revealTop, Math.max(viewport.bottom - viewport.top - 2 * REVEAL_MARGIN, 0));
|
|
195
|
+
const scrollDistance = getScrollDistanceToRevealAnchor(anchorTop + revealTop - REVEAL_MARGIN, revealHeight + 2 * REVEAL_MARGIN, viewport);
|
|
196
|
+
scrollContainer.scrollBy({
|
|
197
|
+
behavior: 'instant',
|
|
198
|
+
top: scrollDistance
|
|
199
|
+
});
|
|
200
|
+
setPopupTop(blockMenuEl, selectedNode.getBoundingClientRect().top + positionSnapshot.popupOffsetFromBlock);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const scrollableContainer = doc.querySelector(EDITOR_CONTENT_CONTAINER_SELECTOR);
|
|
204
|
+
if (!selectedNode || !scrollableContainer) {
|
|
14
205
|
return;
|
|
15
206
|
}
|
|
16
207
|
const parentElement = blockMenuEl.parentElement;
|
|
17
208
|
const currentTop = parentElement.getBoundingClientRect().top;
|
|
18
|
-
const distance =
|
|
209
|
+
const distance = selectedNode.getBoundingClientRect().top - blockMenuEl.getBoundingClientRect().top;
|
|
19
210
|
scrollableContainer.scrollBy({
|
|
20
211
|
behavior: 'instant',
|
|
21
212
|
top: distance
|
|
@@ -31,4 +222,22 @@ export const fixBlockMenuPositionAndScroll = firstSelectedNode => {
|
|
|
31
222
|
const currentTopValue = parseFloat(parentElement.style.top || '0');
|
|
32
223
|
parentElement.style.top = `${currentTopValue + topDifference}px`;
|
|
33
224
|
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Shared by move-up.tsx and move-down.tsx: schedules the post-move fix for the frame after
|
|
229
|
+
* ProseMirror has re-rendered.
|
|
230
|
+
*
|
|
231
|
+
* Falls back to the pre-PR always-scroll path (matching gate-off, via `getFirstSelectedDomNode`)
|
|
232
|
+
* whenever a `positionSnapshot` wasn't captured - e.g. a media/file thumbnail reporting zero height
|
|
233
|
+
* for a frame when the menu opened - rather than silently doing nothing.
|
|
234
|
+
*/
|
|
235
|
+
export const scheduleBlockMenuPositionFix = (positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode) => {
|
|
236
|
+
requestAnimationFrame(() => {
|
|
237
|
+
if (fg('platform_editor_blocks_patch_8') && positionSnapshot) {
|
|
238
|
+
fixBlockMenuPositionAndScroll(getMovedBlockDomNode(), positionSnapshot);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
fixBlockMenuPositionAndScroll(getFirstSelectedDomNode());
|
|
242
|
+
});
|
|
34
243
|
};
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import React, { useCallback, createContext, useContext, useRef } from 'react';
|
|
2
|
+
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
3
|
+
import { getBlockMenuAnchorMetrics } from './utils/fixBlockMenuPositionAndScroll';
|
|
2
4
|
var BlockMenuContext = /*#__PURE__*/createContext({
|
|
3
5
|
onDropdownOpenChanged: function onDropdownOpenChanged() {},
|
|
4
6
|
moveDownRef: /*#__PURE__*/React.createRef(),
|
|
5
7
|
moveUpRef: /*#__PURE__*/React.createRef(),
|
|
6
8
|
getFirstSelectedDomNode: function getFirstSelectedDomNode() {
|
|
7
9
|
return undefined;
|
|
10
|
+
},
|
|
11
|
+
getMovedBlockDomNode: function getMovedBlockDomNode() {
|
|
12
|
+
return undefined;
|
|
13
|
+
},
|
|
14
|
+
getSelectedBlockDomNode: function getSelectedBlockDomNode() {
|
|
15
|
+
return undefined;
|
|
16
|
+
},
|
|
17
|
+
anchorMetricsRef: {
|
|
18
|
+
current: undefined
|
|
8
19
|
}
|
|
9
20
|
});
|
|
10
21
|
export var useBlockMenu = function useBlockMenu() {
|
|
@@ -20,6 +31,7 @@ export var BlockMenuProvider = function BlockMenuProvider(_ref) {
|
|
|
20
31
|
editorView = _ref.editorView;
|
|
21
32
|
var moveUpRef = useRef(null);
|
|
22
33
|
var moveDownRef = useRef(null);
|
|
34
|
+
var anchorMetricsRef = useRef(undefined);
|
|
23
35
|
var getFirstSelectedDomNode = useCallback(function () {
|
|
24
36
|
var _api$selection;
|
|
25
37
|
var from = api === null || api === void 0 || (_api$selection = api.selection) === null || _api$selection === void 0 || (_api$selection = _api$selection.sharedState.currentState()) === null || _api$selection === void 0 || (_api$selection = _api$selection.selection) === null || _api$selection === void 0 ? void 0 : _api$selection.from;
|
|
@@ -30,25 +42,74 @@ export var BlockMenuProvider = function BlockMenuProvider(_ref) {
|
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
44
|
}, [api, editorView]);
|
|
45
|
+
var getTopLevelBlockDomNodeAt = useCallback(function (from) {
|
|
46
|
+
if (!editorView) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
var nodeDOM = editorView.nodeDOM(from);
|
|
50
|
+
var domAtPos = nodeDOM instanceof Element ? nodeDOM : editorView.domAtPos(from).node;
|
|
51
|
+
var element = domAtPos instanceof Element ? domAtPos : domAtPos.parentElement;
|
|
52
|
+
if (!element || !editorView.dom.contains(element)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
var block = element;
|
|
56
|
+
while (block.parentElement && block.parentElement !== editorView.dom) {
|
|
57
|
+
block = block.parentElement;
|
|
58
|
+
}
|
|
59
|
+
return block;
|
|
60
|
+
}, [editorView]);
|
|
61
|
+
var getSelectedBlockDomNode = useCallback(function () {
|
|
62
|
+
var _api$blockControls, _ref2, _blockControlsState$a, _blockControlsState$a2, _blockControlsState$m, _blockControlsState$p;
|
|
63
|
+
var blockControlsState = api === null || api === void 0 || (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.sharedState.currentState();
|
|
64
|
+
// The editor selection is not necessarily in the block the menu was opened for.
|
|
65
|
+
var from = (_ref2 = (_blockControlsState$a = blockControlsState === null || blockControlsState === void 0 || (_blockControlsState$a2 = blockControlsState.activeNode) === null || _blockControlsState$a2 === void 0 ? void 0 : _blockControlsState$a2.pos) !== null && _blockControlsState$a !== void 0 ? _blockControlsState$a : blockControlsState === null || blockControlsState === void 0 || (_blockControlsState$m = blockControlsState.menuTriggerByNode) === null || _blockControlsState$m === void 0 ? void 0 : _blockControlsState$m.pos) !== null && _ref2 !== void 0 ? _ref2 : blockControlsState === null || blockControlsState === void 0 || (_blockControlsState$p = blockControlsState.preservedSelection) === null || _blockControlsState$p === void 0 ? void 0 : _blockControlsState$p.from;
|
|
66
|
+
return from === undefined ? undefined : getTopLevelBlockDomNodeAt(from);
|
|
67
|
+
}, [api, getTopLevelBlockDomNodeAt]);
|
|
68
|
+
var getMovedBlockDomNode = useCallback(function () {
|
|
69
|
+
// A move transaction maps its preserved selection onto the node it just moved, so this is
|
|
70
|
+
// where that node ended up - unlike the block menu state, which keeps stale positions.
|
|
71
|
+
var from = editorView === null || editorView === void 0 ? void 0 : editorView.state.selection.from;
|
|
72
|
+
return from === undefined ? undefined : getTopLevelBlockDomNodeAt(from);
|
|
73
|
+
}, [editorView, getTopLevelBlockDomNodeAt]);
|
|
33
74
|
var onDropdownOpenChanged = useCallback(function (isOpen) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
75
|
+
anchorMetricsRef.current = undefined;
|
|
76
|
+
if (isOpen) {
|
|
77
|
+
if (fg('platform_editor_blocks_patch_8')) {
|
|
78
|
+
// The popup lays itself out next to the drag handle over the next frame or two.
|
|
79
|
+
var remainingFrames = 3;
|
|
80
|
+
var _captureAnchorMetrics = function captureAnchorMetrics() {
|
|
81
|
+
requestAnimationFrame(function () {
|
|
82
|
+
anchorMetricsRef.current = getBlockMenuAnchorMetrics(getSelectedBlockDomNode());
|
|
83
|
+
remainingFrames -= 1;
|
|
84
|
+
if (!anchorMetricsRef.current && remainingFrames > 0) {
|
|
85
|
+
_captureAnchorMetrics();
|
|
86
|
+
}
|
|
40
87
|
});
|
|
41
|
-
}
|
|
42
|
-
|
|
88
|
+
};
|
|
89
|
+
_captureAnchorMetrics();
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
43
92
|
}
|
|
44
|
-
|
|
93
|
+
|
|
94
|
+
// On Dropdown closed, return focus to editor
|
|
95
|
+
setTimeout(function () {
|
|
96
|
+
return requestAnimationFrame(function () {
|
|
97
|
+
api === null || api === void 0 || api.core.actions.focus({
|
|
98
|
+
scrollIntoView: false
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}, 1);
|
|
102
|
+
}, [api, getSelectedBlockDomNode]);
|
|
45
103
|
return /*#__PURE__*/React.createElement(BlockMenuContext.Provider, {
|
|
46
104
|
// eslint-disable-next-line @atlassian/perf-linting/no-inline-context-value, @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
|
|
47
105
|
value: {
|
|
48
106
|
onDropdownOpenChanged: onDropdownOpenChanged,
|
|
49
107
|
moveDownRef: moveDownRef,
|
|
50
108
|
moveUpRef: moveUpRef,
|
|
51
|
-
getFirstSelectedDomNode: getFirstSelectedDomNode
|
|
109
|
+
getFirstSelectedDomNode: getFirstSelectedDomNode,
|
|
110
|
+
getMovedBlockDomNode: getMovedBlockDomNode,
|
|
111
|
+
getSelectedBlockDomNode: getSelectedBlockDomNode,
|
|
112
|
+
anchorMetricsRef: anchorMetricsRef
|
|
52
113
|
}
|
|
53
114
|
}, children);
|
|
54
115
|
};
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/* block-menu.tsx generated by @compiled/babel-plugin v2.0.0 */
|
|
2
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
3
|
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
3
4
|
import "./block-menu.compiled.css";
|
|
4
5
|
import { ax, ix } from "@compiled/react/runtime";
|
|
6
|
+
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; }
|
|
7
|
+
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; }
|
|
5
8
|
import React, { useContext, useEffect, useRef } from 'react';
|
|
6
9
|
import { injectIntl } from 'react-intl';
|
|
7
10
|
import { cx } from '@atlaskit/css';
|
|
@@ -257,8 +260,9 @@ var BlockMenu = function BlockMenu(_ref5) {
|
|
|
257
260
|
if (!isMenuOpen) {
|
|
258
261
|
return;
|
|
259
262
|
}
|
|
263
|
+
onDropdownOpenChanged(true);
|
|
260
264
|
setMenuHeight(((_popupRef$current = popupRef.current) === null || _popupRef$current === void 0 ? void 0 : _popupRef$current.clientHeight) || FALLBACK_MENU_HEIGHT);
|
|
261
|
-
}, [isMenuOpen]);
|
|
265
|
+
}, [isMenuOpen, onDropdownOpenChanged]);
|
|
262
266
|
var hasFocus = (_ref8 = (editorView === null || editorView === void 0 ? void 0 : editorView.hasFocus()) ||
|
|
263
267
|
// eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
|
|
264
268
|
document.activeElement === targetHandleRef || popupRef.current && (
|
|
@@ -347,6 +351,23 @@ var BlockMenu = function BlockMenu(_ref5) {
|
|
|
347
351
|
stick: true
|
|
348
352
|
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
|
|
349
353
|
,
|
|
354
|
+
onPositionCalculated: fg('platform_editor_blocks_patch_8') ? function (position) {
|
|
355
|
+
if (position.left === undefined) {
|
|
356
|
+
// Already placed on the gutter side (using `right`) - nothing to correct.
|
|
357
|
+
return position;
|
|
358
|
+
}
|
|
359
|
+
// Popup prefers whichever side has more room, which flips the menu onto the
|
|
360
|
+
// content side when the gutter is too narrow to fit it. Force it back onto the
|
|
361
|
+
// gutter side instead - it can still overlap the content once there, but it
|
|
362
|
+
// must not start on top of the node. `position.left` is shifted by the same
|
|
363
|
+
// distance regardless of the offset parent/scroll it was calculated against,
|
|
364
|
+
// since that just moves both by the same amount.
|
|
365
|
+
return _objectSpread(_objectSpread({}, position), {}, {
|
|
366
|
+
left: position.left - DEFAULT_MENU_WIDTH - DRAG_HANDLE_WIDTH - 2 * DRAG_HANDLE_OFFSET_PADDING
|
|
367
|
+
});
|
|
368
|
+
} : undefined
|
|
369
|
+
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
|
|
370
|
+
,
|
|
350
371
|
offset: [DRAG_HANDLE_WIDTH + DRAG_HANDLE_OFFSET_PADDING, targetHandleHeightOffset],
|
|
351
372
|
focusTrap: openedViaKeyboard ?
|
|
352
373
|
// Only enable focus trap when opened via keyboard to make sure the focus is on the first focusable menu item
|
package/dist/esm/ui/move-down.js
CHANGED
|
@@ -8,9 +8,10 @@ import { blockMenuMessages as messages } from '@atlaskit/editor-common/messages'
|
|
|
8
8
|
import { DIRECTION } from '@atlaskit/editor-common/types';
|
|
9
9
|
import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
|
|
10
10
|
import ArrowDownIcon from '@atlaskit/icon/core/arrow-down';
|
|
11
|
+
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
11
12
|
import { useBlockMenu } from './block-menu-provider';
|
|
12
13
|
import { BLOCK_MENU_ITEM_NAME } from './consts';
|
|
13
|
-
import {
|
|
14
|
+
import { getBlockMenuPositionSnapshot, scheduleBlockMenuPositionFix } from './utils/fixBlockMenuPositionAndScroll';
|
|
14
15
|
var MoveDownDropdownItemContent = function MoveDownDropdownItemContent(_ref) {
|
|
15
16
|
var api = _ref.api;
|
|
16
17
|
var _useIntl = useIntl(),
|
|
@@ -18,7 +19,10 @@ var MoveDownDropdownItemContent = function MoveDownDropdownItemContent(_ref) {
|
|
|
18
19
|
var _useBlockMenu = useBlockMenu(),
|
|
19
20
|
moveUpRef = _useBlockMenu.moveUpRef,
|
|
20
21
|
moveDownRef = _useBlockMenu.moveDownRef,
|
|
21
|
-
getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode
|
|
22
|
+
getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode,
|
|
23
|
+
getMovedBlockDomNode = _useBlockMenu.getMovedBlockDomNode,
|
|
24
|
+
getSelectedBlockDomNode = _useBlockMenu.getSelectedBlockDomNode,
|
|
25
|
+
anchorMetricsRef = _useBlockMenu.anchorMetricsRef;
|
|
22
26
|
var _useSharedPluginState = useSharedPluginStateWithSelector(api, ['blockControls'], function (_ref2) {
|
|
23
27
|
var _blockControlsState$b;
|
|
24
28
|
var blockControlsState = _ref2.blockControlsState;
|
|
@@ -36,6 +40,7 @@ var MoveDownDropdownItemContent = function MoveDownDropdownItemContent(_ref) {
|
|
|
36
40
|
}
|
|
37
41
|
}, [canMoveDown, moveUpRef, moveDownRef]);
|
|
38
42
|
var handleClick = function handleClick() {
|
|
43
|
+
var positionSnapshot = fg('platform_editor_blocks_patch_8') ? getBlockMenuPositionSnapshot(getSelectedBlockDomNode(), anchorMetricsRef.current) : undefined;
|
|
39
44
|
api === null || api === void 0 || api.core.actions.execute(function (_ref3) {
|
|
40
45
|
var _api$analytics, _api$blockControls;
|
|
41
46
|
var tr = _ref3.tr;
|
|
@@ -53,10 +58,7 @@ var MoveDownDropdownItemContent = function MoveDownDropdownItemContent(_ref) {
|
|
|
53
58
|
});
|
|
54
59
|
return tr;
|
|
55
60
|
});
|
|
56
|
-
|
|
57
|
-
var newFirstNode = getFirstSelectedDomNode();
|
|
58
|
-
fixBlockMenuPositionAndScroll(newFirstNode);
|
|
59
|
-
});
|
|
61
|
+
scheduleBlockMenuPositionFix(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode);
|
|
60
62
|
};
|
|
61
63
|
return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
|
|
62
64
|
triggerRef: moveDownRef,
|
package/dist/esm/ui/move-up.js
CHANGED
|
@@ -8,9 +8,10 @@ import { blockMenuMessages as messages } from '@atlaskit/editor-common/messages'
|
|
|
8
8
|
import { DIRECTION } from '@atlaskit/editor-common/types';
|
|
9
9
|
import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
|
|
10
10
|
import ArrowUpIcon from '@atlaskit/icon/core/arrow-up';
|
|
11
|
+
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
11
12
|
import { useBlockMenu } from './block-menu-provider';
|
|
12
13
|
import { BLOCK_MENU_ITEM_NAME } from './consts';
|
|
13
|
-
import {
|
|
14
|
+
import { getBlockMenuPositionSnapshot, scheduleBlockMenuPositionFix } from './utils/fixBlockMenuPositionAndScroll';
|
|
14
15
|
var MoveUpDropdownItemContent = function MoveUpDropdownItemContent(_ref) {
|
|
15
16
|
var api = _ref.api;
|
|
16
17
|
var _useIntl = useIntl(),
|
|
@@ -18,7 +19,10 @@ var MoveUpDropdownItemContent = function MoveUpDropdownItemContent(_ref) {
|
|
|
18
19
|
var _useBlockMenu = useBlockMenu(),
|
|
19
20
|
moveUpRef = _useBlockMenu.moveUpRef,
|
|
20
21
|
moveDownRef = _useBlockMenu.moveDownRef,
|
|
21
|
-
getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode
|
|
22
|
+
getFirstSelectedDomNode = _useBlockMenu.getFirstSelectedDomNode,
|
|
23
|
+
getMovedBlockDomNode = _useBlockMenu.getMovedBlockDomNode,
|
|
24
|
+
getSelectedBlockDomNode = _useBlockMenu.getSelectedBlockDomNode,
|
|
25
|
+
anchorMetricsRef = _useBlockMenu.anchorMetricsRef;
|
|
22
26
|
var _useSharedPluginState = useSharedPluginStateWithSelector(api, ['blockControls'], function (_ref2) {
|
|
23
27
|
var _blockControlsState$b;
|
|
24
28
|
var blockControlsState = _ref2.blockControlsState;
|
|
@@ -34,6 +38,7 @@ var MoveUpDropdownItemContent = function MoveUpDropdownItemContent(_ref) {
|
|
|
34
38
|
}
|
|
35
39
|
}, [canMoveUp, moveDownRef, moveUpRef]);
|
|
36
40
|
var handleClick = function handleClick() {
|
|
41
|
+
var positionSnapshot = fg('platform_editor_blocks_patch_8') ? getBlockMenuPositionSnapshot(getSelectedBlockDomNode(), anchorMetricsRef.current) : undefined;
|
|
37
42
|
api === null || api === void 0 || api.core.actions.execute(function (_ref3) {
|
|
38
43
|
var _api$analytics, _api$blockControls;
|
|
39
44
|
var tr = _ref3.tr;
|
|
@@ -51,10 +56,7 @@ var MoveUpDropdownItemContent = function MoveUpDropdownItemContent(_ref) {
|
|
|
51
56
|
});
|
|
52
57
|
return tr;
|
|
53
58
|
});
|
|
54
|
-
|
|
55
|
-
var newFirstNode = getFirstSelectedDomNode();
|
|
56
|
-
fixBlockMenuPositionAndScroll(newFirstNode);
|
|
57
|
-
});
|
|
59
|
+
scheduleBlockMenuPositionFix(positionSnapshot, getMovedBlockDomNode, getFirstSelectedDomNode);
|
|
58
60
|
};
|
|
59
61
|
return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
|
|
60
62
|
triggerRef: moveUpRef,
|