@atlaskit/editor-plugin-paste-options-toolbar 9.1.4 → 9.1.6
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 +18 -0
- package/dist/cjs/pm-plugins/util/format-handlers.js +1 -1
- package/dist/cjs/ui/on-paste-actions-menu/PasteActionsMenu.js +93 -7
- package/dist/es2019/pm-plugins/util/format-handlers.js +1 -1
- package/dist/es2019/ui/on-paste-actions-menu/PasteActionsMenu.js +92 -8
- package/dist/esm/pm-plugins/util/format-handlers.js +1 -1
- package/dist/esm/ui/on-paste-actions-menu/PasteActionsMenu.js +92 -8
- package/dist/types/ui/on-paste-actions-menu/PasteActionsMenu.d.ts +27 -4
- package/dist/types-ts4.5/ui/on-paste-actions-menu/PasteActionsMenu.d.ts +27 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-paste-options-toolbar
|
|
2
2
|
|
|
3
|
+
## 9.1.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`fc5915138b437`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/fc5915138b437) -
|
|
8
|
+
EDITOR-6110 Ensure that paste actions menu appears in the correct position when the first item of
|
|
9
|
+
the paste is an inline node or mark
|
|
10
|
+
- Updated dependencies
|
|
11
|
+
|
|
12
|
+
## 9.1.5
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [`2e030e319c013`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/2e030e319c013) -
|
|
17
|
+
[EDITOR-5982] Fix AI paste menu not appearing when pasting heading + text
|
|
18
|
+
- [`e7032ec0e9287`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/e7032ec0e9287) -
|
|
19
|
+
[ux] EDITOR-5983 Ensure paste menu is in correct position when first pasted node is empty
|
|
20
|
+
|
|
3
21
|
## 9.1.4
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -157,7 +157,7 @@ function getMarkdownSlice(text, schema, selection) {
|
|
|
157
157
|
for (var i = 0; i < textSplitByCodeBlock.length; i++) {
|
|
158
158
|
if (i % 2 === 0) {
|
|
159
159
|
// Ignored via go/ees005
|
|
160
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
160
|
+
// eslint-disable-next-line require-unicode-regexp, @atlassian/perf-linting/no-expensive-split-replace -- Ignored via go/ees017 (to be fixed)
|
|
161
161
|
textSplitByCodeBlock[i] = textSplitByCodeBlock[i].replace(/\\/g, '\\\\');
|
|
162
162
|
}
|
|
163
163
|
}
|
|
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
value: true
|
|
7
7
|
});
|
|
8
8
|
exports.PasteActionsMenu = void 0;
|
|
9
|
+
exports.findBlockAncestorDOM = findBlockAncestorDOM;
|
|
10
|
+
exports.getTargetElement = getTargetElement;
|
|
9
11
|
exports.getVisualEndBottom = getVisualEndBottom;
|
|
10
12
|
exports.onPositionCalculated = onPositionCalculated;
|
|
11
13
|
exports.resolveTableAfterPos = resolveTableAfterPos;
|
|
@@ -29,10 +31,21 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
29
31
|
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; }
|
|
30
32
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
31
33
|
var PopupWithListeners = (0, _uiReact.withReactEditorViewOuterListeners)(_ui.Popup);
|
|
34
|
+
/**
|
|
35
|
+
* Returns the DOM element at the given document position for use as a Popup anchor.
|
|
36
|
+
* For empty blocks (BR elements), returns the parent element to ensure correct positioning.
|
|
37
|
+
*/
|
|
32
38
|
function getTargetElement(editorView, pos) {
|
|
33
39
|
try {
|
|
34
40
|
var domRef = (0, _utils.findDomRefAtPos)(pos, editorView.domAtPos.bind(editorView));
|
|
35
41
|
if (domRef instanceof HTMLElement) {
|
|
42
|
+
// Empty blocks render a <br> placeholder whose bounding rect has no
|
|
43
|
+
// meaningful dimensions (height ≈ 0). Using it as the Popup anchor
|
|
44
|
+
// causes the menu to appear at an unexpected position. Walk up to the
|
|
45
|
+
// parent block element so the Popup anchors correctly.
|
|
46
|
+
if (domRef.nodeName === 'BR' && domRef.parentElement) {
|
|
47
|
+
return domRef.parentElement;
|
|
48
|
+
}
|
|
36
49
|
return domRef;
|
|
37
50
|
}
|
|
38
51
|
return null;
|
|
@@ -70,10 +83,47 @@ function getVisualEndBottom(editorView, pasteEndPos, tableAfterPos) {
|
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
86
|
+
* Finds the DOM element for the nearest block-level ProseMirror ancestor of
|
|
87
|
+
* the given document position. Uses ProseMirror's schema (`node.isBlock`)
|
|
88
|
+
* rather than CSS display properties, so the check is always in sync with the
|
|
89
|
+
* document model.
|
|
90
|
+
*
|
|
91
|
+
* Returns `null` if no block ancestor can be resolved to a DOM element.
|
|
92
|
+
*/
|
|
93
|
+
function findBlockAncestorDOM(editorView, pos) {
|
|
94
|
+
try {
|
|
95
|
+
var $pos = editorView.state.doc.resolve(pos);
|
|
96
|
+
// Walk up the document tree from the resolved position's innermost
|
|
97
|
+
// node towards the root. $pos.node(depth) gives the ancestor at each
|
|
98
|
+
// depth; $pos.start(depth) gives the position just inside that ancestor,
|
|
99
|
+
// so `$pos.start(depth) - 1` is the position of the ancestor node itself
|
|
100
|
+
// (which is what nodeDOM expects).
|
|
101
|
+
for (var depth = $pos.depth; depth >= 0; depth--) {
|
|
102
|
+
var node = $pos.node(depth);
|
|
103
|
+
if (node.isBlock) {
|
|
104
|
+
var domNode = editorView.nodeDOM($pos.start(depth) - 1);
|
|
105
|
+
if (domNode instanceof HTMLElement) {
|
|
106
|
+
return domNode;
|
|
107
|
+
}
|
|
108
|
+
// depth 0 is the doc node — nodeDOM(–1) won't work, so try
|
|
109
|
+
// the editor's own DOM element as a fallback.
|
|
110
|
+
if (depth === 0 && editorView.dom instanceof HTMLElement) {
|
|
111
|
+
return editorView.dom;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (_unused2) {
|
|
116
|
+
// Position may be out of range after a concurrent edit — fall through.
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Adjusts the position of the paste menu so that:
|
|
123
|
+
*
|
|
124
|
+
* **Vertical:** The menu aligns with the top of the pasted content using the
|
|
125
|
+
* exact coordinates at the paste start position, and sticks to the top of the
|
|
126
|
+
* scroll container when the pasted content scrolls above the visible area.
|
|
77
127
|
*
|
|
78
128
|
* The Popup uses alignY="bottom", which positions the popup below the target
|
|
79
129
|
* element's bottom edge. This override:
|
|
@@ -84,16 +134,26 @@ function getVisualEndBottom(editorView, pasteEndPos, tableAfterPos) {
|
|
|
84
134
|
* to the scroll container's top edge (sticky-top).
|
|
85
135
|
* 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
|
|
86
136
|
* above the visible area.
|
|
137
|
+
*
|
|
138
|
+
* **Horizontal:** When the target element is an inline element (e.g. a mark
|
|
139
|
+
* wrapper like `<strong>`, or an inline node like an emoji), the Popup's
|
|
140
|
+
* `alignX="end"` would place the menu at the right edge of that narrow
|
|
141
|
+
* element. This override resolves the nearest block-level ProseMirror
|
|
142
|
+
* ancestor (using `node.isBlock` from the document schema) and re-anchors
|
|
143
|
+
* the horizontal position to its right edge, so the menu consistently
|
|
144
|
+
* appears at the right side of the content area.
|
|
87
145
|
*/
|
|
88
146
|
function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, targetElement, scrollableElement) {
|
|
89
147
|
// Pre-compute once per render to avoid doc.resolve() on every scroll frame.
|
|
90
148
|
var tableAfterPos = resolveTableAfterPos(editorView, pasteEndPos);
|
|
149
|
+
var blockAncestorDOM = findBlockAncestorDOM(editorView, pasteStartPos);
|
|
91
150
|
return function (position) {
|
|
92
151
|
var _position$top;
|
|
93
152
|
var startCoords = editorView.coordsAtPos(pasteStartPos);
|
|
94
153
|
var endBottom = getVisualEndBottom(editorView, pasteEndPos, tableAfterPos);
|
|
95
154
|
var targetRect = targetElement.getBoundingClientRect();
|
|
96
155
|
|
|
156
|
+
// ── Vertical adjustment ──────────────────────────────────────────
|
|
97
157
|
// The Popup places the menu at the target's bottom edge by default.
|
|
98
158
|
// We shift it up so it aligns with the paste start position.
|
|
99
159
|
// Both coordinates are in viewport space, so the delta is offset-parent agnostic.
|
|
@@ -109,8 +169,28 @@ function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, targetElem
|
|
|
109
169
|
adjustedTop += scrollContainerTop - startCoords.top + _constants.PASTE_MENU_GAP_TOP;
|
|
110
170
|
}
|
|
111
171
|
}
|
|
172
|
+
|
|
173
|
+
// ── Horizontal adjustment ────────────────────────────────────────
|
|
174
|
+
// When pasted content starts with a mark (bold, italic, link …) or
|
|
175
|
+
// an inline node (emoji, smart link, inline image …),
|
|
176
|
+
// findDomRefAtPos returns the narrow inline wrapper element. The
|
|
177
|
+
// Popup's alignX="end" then places the menu at that element's right
|
|
178
|
+
// edge instead of the content area's right edge. We correct this by
|
|
179
|
+
// resolving the nearest block-level ProseMirror ancestor and
|
|
180
|
+
// re-anchoring to its right edge.
|
|
181
|
+
var adjustedLeft = position.left;
|
|
182
|
+
if (blockAncestorDOM && blockAncestorDOM !== targetElement) {
|
|
183
|
+
var _position$left;
|
|
184
|
+
var blockRect = blockAncestorDOM.getBoundingClientRect();
|
|
185
|
+
// Shift left by the difference between the block's right edge and
|
|
186
|
+
// the inline target's right edge. This mirrors what alignX="end"
|
|
187
|
+
// would have computed if the target were the block element.
|
|
188
|
+
var leftDelta = blockRect.right - targetRect.right;
|
|
189
|
+
adjustedLeft = ((_position$left = position.left) !== null && _position$left !== void 0 ? _position$left : 0) + leftDelta;
|
|
190
|
+
}
|
|
112
191
|
return _objectSpread(_objectSpread({}, position), {}, {
|
|
113
|
-
top: adjustedTop
|
|
192
|
+
top: adjustedTop,
|
|
193
|
+
left: adjustedLeft
|
|
114
194
|
});
|
|
115
195
|
};
|
|
116
196
|
}
|
|
@@ -147,7 +227,12 @@ var PasteActionsMenu = exports.PasteActionsMenu = function PasteActionsMenu(_ref
|
|
|
147
227
|
var $pos = editorView.state.doc.resolve(lastContentPasted.pasteStartPos);
|
|
148
228
|
var pasteAncestorNodeNames = [];
|
|
149
229
|
for (var depth = $pos.depth; depth > 0; depth--) {
|
|
150
|
-
|
|
230
|
+
// Only include an ancestor if the entire pasted range is contained within it.
|
|
231
|
+
// This prevents nodes like 'heading' from being flagged as ancestors when the
|
|
232
|
+
// pasted content starts in a heading but extends beyond it (e.g. heading + paragraph).
|
|
233
|
+
if (lastContentPasted.pasteEndPos <= $pos.end(depth)) {
|
|
234
|
+
pasteAncestorNodeNames.push($pos.node(depth).type.name);
|
|
235
|
+
}
|
|
151
236
|
}
|
|
152
237
|
var legacyVisible = (0, _toolbar2.isToolbarVisible)(editorView.state, lastContentPasted) && ((_lastContentPasted$te = (_lastContentPasted$te2 = lastContentPasted.text) === null || _lastContentPasted$te2 === void 0 ? void 0 : _lastContentPasted$te2.length) !== null && _lastContentPasted$te !== void 0 ? _lastContentPasted$te : 0) >= 100;
|
|
153
238
|
(0, _commands.showToolbar)(lastContentPasted, selectedOption, legacyVisible, pasteAncestorNodeNames)(editorView.state, editorView.dispatch);
|
|
@@ -231,7 +316,8 @@ var PasteActionsMenu = exports.PasteActionsMenu = function PasteActionsMenu(_ref
|
|
|
231
316
|
minPopupMargin: _constants.PASTE_MENU_GAP_HORIZONTAL,
|
|
232
317
|
zIndex: _editorSharedStyles.akEditorFloatingPanelZIndex,
|
|
233
318
|
alignX: "end",
|
|
234
|
-
alignY: "bottom"
|
|
319
|
+
alignY: "bottom"
|
|
320
|
+
/* eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) */,
|
|
235
321
|
offset: [_constants.PASTE_MENU_GAP_HORIZONTAL, 0],
|
|
236
322
|
onPositionCalculated: onPositionCalculated(editorView, pasteStartPos, pasteEndPos, target, effectiveScrollableElement),
|
|
237
323
|
handleClickOutside: handleClickOutside,
|
|
@@ -152,7 +152,7 @@ export function getMarkdownSlice(text, schema, selection) {
|
|
|
152
152
|
for (let i = 0; i < textSplitByCodeBlock.length; i++) {
|
|
153
153
|
if (i % 2 === 0) {
|
|
154
154
|
// Ignored via go/ees005
|
|
155
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
155
|
+
// eslint-disable-next-line require-unicode-regexp, @atlassian/perf-linting/no-expensive-split-replace -- Ignored via go/ees017 (to be fixed)
|
|
156
156
|
textSplitByCodeBlock[i] = textSplitByCodeBlock[i].replace(/\\/g, '\\\\');
|
|
157
157
|
}
|
|
158
158
|
}
|
|
@@ -14,10 +14,21 @@ import { isToolbarVisible } from '../toolbar';
|
|
|
14
14
|
import { getVisibleKeys, hasVisibleButton } from './hasVisibleButton';
|
|
15
15
|
import { PasteActionsMenuContent } from './PasteActionsMenuContent';
|
|
16
16
|
const PopupWithListeners = withReactEditorViewOuterListeners(Popup);
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Returns the DOM element at the given document position for use as a Popup anchor.
|
|
19
|
+
* For empty blocks (BR elements), returns the parent element to ensure correct positioning.
|
|
20
|
+
*/
|
|
21
|
+
export function getTargetElement(editorView, pos) {
|
|
18
22
|
try {
|
|
19
23
|
const domRef = findDomRefAtPos(pos, editorView.domAtPos.bind(editorView));
|
|
20
24
|
if (domRef instanceof HTMLElement) {
|
|
25
|
+
// Empty blocks render a <br> placeholder whose bounding rect has no
|
|
26
|
+
// meaningful dimensions (height ≈ 0). Using it as the Popup anchor
|
|
27
|
+
// causes the menu to appear at an unexpected position. Walk up to the
|
|
28
|
+
// parent block element so the Popup anchors correctly.
|
|
29
|
+
if (domRef.nodeName === 'BR' && domRef.parentElement) {
|
|
30
|
+
return domRef.parentElement;
|
|
31
|
+
}
|
|
21
32
|
return domRef;
|
|
22
33
|
}
|
|
23
34
|
return null;
|
|
@@ -55,10 +66,47 @@ export function getVisualEndBottom(editorView, pasteEndPos, tableAfterPos) {
|
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
69
|
+
* Finds the DOM element for the nearest block-level ProseMirror ancestor of
|
|
70
|
+
* the given document position. Uses ProseMirror's schema (`node.isBlock`)
|
|
71
|
+
* rather than CSS display properties, so the check is always in sync with the
|
|
72
|
+
* document model.
|
|
73
|
+
*
|
|
74
|
+
* Returns `null` if no block ancestor can be resolved to a DOM element.
|
|
75
|
+
*/
|
|
76
|
+
export function findBlockAncestorDOM(editorView, pos) {
|
|
77
|
+
try {
|
|
78
|
+
const $pos = editorView.state.doc.resolve(pos);
|
|
79
|
+
// Walk up the document tree from the resolved position's innermost
|
|
80
|
+
// node towards the root. $pos.node(depth) gives the ancestor at each
|
|
81
|
+
// depth; $pos.start(depth) gives the position just inside that ancestor,
|
|
82
|
+
// so `$pos.start(depth) - 1` is the position of the ancestor node itself
|
|
83
|
+
// (which is what nodeDOM expects).
|
|
84
|
+
for (let depth = $pos.depth; depth >= 0; depth--) {
|
|
85
|
+
const node = $pos.node(depth);
|
|
86
|
+
if (node.isBlock) {
|
|
87
|
+
const domNode = editorView.nodeDOM($pos.start(depth) - 1);
|
|
88
|
+
if (domNode instanceof HTMLElement) {
|
|
89
|
+
return domNode;
|
|
90
|
+
}
|
|
91
|
+
// depth 0 is the doc node — nodeDOM(–1) won't work, so try
|
|
92
|
+
// the editor's own DOM element as a fallback.
|
|
93
|
+
if (depth === 0 && editorView.dom instanceof HTMLElement) {
|
|
94
|
+
return editorView.dom;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Position may be out of range after a concurrent edit — fall through.
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Adjusts the position of the paste menu so that:
|
|
106
|
+
*
|
|
107
|
+
* **Vertical:** The menu aligns with the top of the pasted content using the
|
|
108
|
+
* exact coordinates at the paste start position, and sticks to the top of the
|
|
109
|
+
* scroll container when the pasted content scrolls above the visible area.
|
|
62
110
|
*
|
|
63
111
|
* The Popup uses alignY="bottom", which positions the popup below the target
|
|
64
112
|
* element's bottom edge. This override:
|
|
@@ -69,16 +117,26 @@ export function getVisualEndBottom(editorView, pasteEndPos, tableAfterPos) {
|
|
|
69
117
|
* to the scroll container's top edge (sticky-top).
|
|
70
118
|
* 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
|
|
71
119
|
* above the visible area.
|
|
120
|
+
*
|
|
121
|
+
* **Horizontal:** When the target element is an inline element (e.g. a mark
|
|
122
|
+
* wrapper like `<strong>`, or an inline node like an emoji), the Popup's
|
|
123
|
+
* `alignX="end"` would place the menu at the right edge of that narrow
|
|
124
|
+
* element. This override resolves the nearest block-level ProseMirror
|
|
125
|
+
* ancestor (using `node.isBlock` from the document schema) and re-anchors
|
|
126
|
+
* the horizontal position to its right edge, so the menu consistently
|
|
127
|
+
* appears at the right side of the content area.
|
|
72
128
|
*/
|
|
73
129
|
export function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, targetElement, scrollableElement) {
|
|
74
130
|
// Pre-compute once per render to avoid doc.resolve() on every scroll frame.
|
|
75
131
|
const tableAfterPos = resolveTableAfterPos(editorView, pasteEndPos);
|
|
132
|
+
const blockAncestorDOM = findBlockAncestorDOM(editorView, pasteStartPos);
|
|
76
133
|
return position => {
|
|
77
134
|
var _position$top;
|
|
78
135
|
const startCoords = editorView.coordsAtPos(pasteStartPos);
|
|
79
136
|
const endBottom = getVisualEndBottom(editorView, pasteEndPos, tableAfterPos);
|
|
80
137
|
const targetRect = targetElement.getBoundingClientRect();
|
|
81
138
|
|
|
139
|
+
// ── Vertical adjustment ──────────────────────────────────────────
|
|
82
140
|
// The Popup places the menu at the target's bottom edge by default.
|
|
83
141
|
// We shift it up so it aligns with the paste start position.
|
|
84
142
|
// Both coordinates are in viewport space, so the delta is offset-parent agnostic.
|
|
@@ -94,9 +152,29 @@ export function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, tar
|
|
|
94
152
|
adjustedTop += scrollContainerTop - startCoords.top + PASTE_MENU_GAP_TOP;
|
|
95
153
|
}
|
|
96
154
|
}
|
|
155
|
+
|
|
156
|
+
// ── Horizontal adjustment ────────────────────────────────────────
|
|
157
|
+
// When pasted content starts with a mark (bold, italic, link …) or
|
|
158
|
+
// an inline node (emoji, smart link, inline image …),
|
|
159
|
+
// findDomRefAtPos returns the narrow inline wrapper element. The
|
|
160
|
+
// Popup's alignX="end" then places the menu at that element's right
|
|
161
|
+
// edge instead of the content area's right edge. We correct this by
|
|
162
|
+
// resolving the nearest block-level ProseMirror ancestor and
|
|
163
|
+
// re-anchoring to its right edge.
|
|
164
|
+
let adjustedLeft = position.left;
|
|
165
|
+
if (blockAncestorDOM && blockAncestorDOM !== targetElement) {
|
|
166
|
+
var _position$left;
|
|
167
|
+
const blockRect = blockAncestorDOM.getBoundingClientRect();
|
|
168
|
+
// Shift left by the difference between the block's right edge and
|
|
169
|
+
// the inline target's right edge. This mirrors what alignX="end"
|
|
170
|
+
// would have computed if the target were the block element.
|
|
171
|
+
const leftDelta = blockRect.right - targetRect.right;
|
|
172
|
+
adjustedLeft = ((_position$left = position.left) !== null && _position$left !== void 0 ? _position$left : 0) + leftDelta;
|
|
173
|
+
}
|
|
97
174
|
return {
|
|
98
175
|
...position,
|
|
99
|
-
top: adjustedTop
|
|
176
|
+
top: adjustedTop,
|
|
177
|
+
left: adjustedLeft
|
|
100
178
|
};
|
|
101
179
|
};
|
|
102
180
|
}
|
|
@@ -135,7 +213,12 @@ export const PasteActionsMenu = ({
|
|
|
135
213
|
const $pos = editorView.state.doc.resolve(lastContentPasted.pasteStartPos);
|
|
136
214
|
const pasteAncestorNodeNames = [];
|
|
137
215
|
for (let depth = $pos.depth; depth > 0; depth--) {
|
|
138
|
-
|
|
216
|
+
// Only include an ancestor if the entire pasted range is contained within it.
|
|
217
|
+
// This prevents nodes like 'heading' from being flagged as ancestors when the
|
|
218
|
+
// pasted content starts in a heading but extends beyond it (e.g. heading + paragraph).
|
|
219
|
+
if (lastContentPasted.pasteEndPos <= $pos.end(depth)) {
|
|
220
|
+
pasteAncestorNodeNames.push($pos.node(depth).type.name);
|
|
221
|
+
}
|
|
139
222
|
}
|
|
140
223
|
const legacyVisible = isToolbarVisible(editorView.state, lastContentPasted) && ((_lastContentPasted$te = (_lastContentPasted$te2 = lastContentPasted.text) === null || _lastContentPasted$te2 === void 0 ? void 0 : _lastContentPasted$te2.length) !== null && _lastContentPasted$te !== void 0 ? _lastContentPasted$te : 0) >= 100;
|
|
141
224
|
showToolbar(lastContentPasted, selectedOption, legacyVisible, pasteAncestorNodeNames)(editorView.state, editorView.dispatch);
|
|
@@ -220,7 +303,8 @@ export const PasteActionsMenu = ({
|
|
|
220
303
|
minPopupMargin: PASTE_MENU_GAP_HORIZONTAL,
|
|
221
304
|
zIndex: akEditorFloatingPanelZIndex,
|
|
222
305
|
alignX: "end",
|
|
223
|
-
alignY: "bottom"
|
|
306
|
+
alignY: "bottom"
|
|
307
|
+
/* eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) */,
|
|
224
308
|
offset: [PASTE_MENU_GAP_HORIZONTAL, 0],
|
|
225
309
|
onPositionCalculated: onPositionCalculated(editorView, pasteStartPos, pasteEndPos, target, effectiveScrollableElement),
|
|
226
310
|
handleClickOutside: handleClickOutside,
|
|
@@ -150,7 +150,7 @@ export function getMarkdownSlice(text, schema, selection) {
|
|
|
150
150
|
for (var i = 0; i < textSplitByCodeBlock.length; i++) {
|
|
151
151
|
if (i % 2 === 0) {
|
|
152
152
|
// Ignored via go/ees005
|
|
153
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
153
|
+
// eslint-disable-next-line require-unicode-regexp, @atlassian/perf-linting/no-expensive-split-replace -- Ignored via go/ees017 (to be fixed)
|
|
154
154
|
textSplitByCodeBlock[i] = textSplitByCodeBlock[i].replace(/\\/g, '\\\\');
|
|
155
155
|
}
|
|
156
156
|
}
|
|
@@ -17,10 +17,21 @@ import { isToolbarVisible } from '../toolbar';
|
|
|
17
17
|
import { getVisibleKeys, hasVisibleButton } from './hasVisibleButton';
|
|
18
18
|
import { PasteActionsMenuContent } from './PasteActionsMenuContent';
|
|
19
19
|
var PopupWithListeners = withReactEditorViewOuterListeners(Popup);
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Returns the DOM element at the given document position for use as a Popup anchor.
|
|
22
|
+
* For empty blocks (BR elements), returns the parent element to ensure correct positioning.
|
|
23
|
+
*/
|
|
24
|
+
export function getTargetElement(editorView, pos) {
|
|
21
25
|
try {
|
|
22
26
|
var domRef = findDomRefAtPos(pos, editorView.domAtPos.bind(editorView));
|
|
23
27
|
if (domRef instanceof HTMLElement) {
|
|
28
|
+
// Empty blocks render a <br> placeholder whose bounding rect has no
|
|
29
|
+
// meaningful dimensions (height ≈ 0). Using it as the Popup anchor
|
|
30
|
+
// causes the menu to appear at an unexpected position. Walk up to the
|
|
31
|
+
// parent block element so the Popup anchors correctly.
|
|
32
|
+
if (domRef.nodeName === 'BR' && domRef.parentElement) {
|
|
33
|
+
return domRef.parentElement;
|
|
34
|
+
}
|
|
24
35
|
return domRef;
|
|
25
36
|
}
|
|
26
37
|
return null;
|
|
@@ -58,10 +69,47 @@ export function getVisualEndBottom(editorView, pasteEndPos, tableAfterPos) {
|
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
72
|
+
* Finds the DOM element for the nearest block-level ProseMirror ancestor of
|
|
73
|
+
* the given document position. Uses ProseMirror's schema (`node.isBlock`)
|
|
74
|
+
* rather than CSS display properties, so the check is always in sync with the
|
|
75
|
+
* document model.
|
|
76
|
+
*
|
|
77
|
+
* Returns `null` if no block ancestor can be resolved to a DOM element.
|
|
78
|
+
*/
|
|
79
|
+
export function findBlockAncestorDOM(editorView, pos) {
|
|
80
|
+
try {
|
|
81
|
+
var $pos = editorView.state.doc.resolve(pos);
|
|
82
|
+
// Walk up the document tree from the resolved position's innermost
|
|
83
|
+
// node towards the root. $pos.node(depth) gives the ancestor at each
|
|
84
|
+
// depth; $pos.start(depth) gives the position just inside that ancestor,
|
|
85
|
+
// so `$pos.start(depth) - 1` is the position of the ancestor node itself
|
|
86
|
+
// (which is what nodeDOM expects).
|
|
87
|
+
for (var depth = $pos.depth; depth >= 0; depth--) {
|
|
88
|
+
var node = $pos.node(depth);
|
|
89
|
+
if (node.isBlock) {
|
|
90
|
+
var domNode = editorView.nodeDOM($pos.start(depth) - 1);
|
|
91
|
+
if (domNode instanceof HTMLElement) {
|
|
92
|
+
return domNode;
|
|
93
|
+
}
|
|
94
|
+
// depth 0 is the doc node — nodeDOM(–1) won't work, so try
|
|
95
|
+
// the editor's own DOM element as a fallback.
|
|
96
|
+
if (depth === 0 && editorView.dom instanceof HTMLElement) {
|
|
97
|
+
return editorView.dom;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (_unused2) {
|
|
102
|
+
// Position may be out of range after a concurrent edit — fall through.
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Adjusts the position of the paste menu so that:
|
|
109
|
+
*
|
|
110
|
+
* **Vertical:** The menu aligns with the top of the pasted content using the
|
|
111
|
+
* exact coordinates at the paste start position, and sticks to the top of the
|
|
112
|
+
* scroll container when the pasted content scrolls above the visible area.
|
|
65
113
|
*
|
|
66
114
|
* The Popup uses alignY="bottom", which positions the popup below the target
|
|
67
115
|
* element's bottom edge. This override:
|
|
@@ -72,16 +120,26 @@ export function getVisualEndBottom(editorView, pasteEndPos, tableAfterPos) {
|
|
|
72
120
|
* to the scroll container's top edge (sticky-top).
|
|
73
121
|
* 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
|
|
74
122
|
* above the visible area.
|
|
123
|
+
*
|
|
124
|
+
* **Horizontal:** When the target element is an inline element (e.g. a mark
|
|
125
|
+
* wrapper like `<strong>`, or an inline node like an emoji), the Popup's
|
|
126
|
+
* `alignX="end"` would place the menu at the right edge of that narrow
|
|
127
|
+
* element. This override resolves the nearest block-level ProseMirror
|
|
128
|
+
* ancestor (using `node.isBlock` from the document schema) and re-anchors
|
|
129
|
+
* the horizontal position to its right edge, so the menu consistently
|
|
130
|
+
* appears at the right side of the content area.
|
|
75
131
|
*/
|
|
76
132
|
export function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, targetElement, scrollableElement) {
|
|
77
133
|
// Pre-compute once per render to avoid doc.resolve() on every scroll frame.
|
|
78
134
|
var tableAfterPos = resolveTableAfterPos(editorView, pasteEndPos);
|
|
135
|
+
var blockAncestorDOM = findBlockAncestorDOM(editorView, pasteStartPos);
|
|
79
136
|
return function (position) {
|
|
80
137
|
var _position$top;
|
|
81
138
|
var startCoords = editorView.coordsAtPos(pasteStartPos);
|
|
82
139
|
var endBottom = getVisualEndBottom(editorView, pasteEndPos, tableAfterPos);
|
|
83
140
|
var targetRect = targetElement.getBoundingClientRect();
|
|
84
141
|
|
|
142
|
+
// ── Vertical adjustment ──────────────────────────────────────────
|
|
85
143
|
// The Popup places the menu at the target's bottom edge by default.
|
|
86
144
|
// We shift it up so it aligns with the paste start position.
|
|
87
145
|
// Both coordinates are in viewport space, so the delta is offset-parent agnostic.
|
|
@@ -97,8 +155,28 @@ export function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, tar
|
|
|
97
155
|
adjustedTop += scrollContainerTop - startCoords.top + PASTE_MENU_GAP_TOP;
|
|
98
156
|
}
|
|
99
157
|
}
|
|
158
|
+
|
|
159
|
+
// ── Horizontal adjustment ────────────────────────────────────────
|
|
160
|
+
// When pasted content starts with a mark (bold, italic, link …) or
|
|
161
|
+
// an inline node (emoji, smart link, inline image …),
|
|
162
|
+
// findDomRefAtPos returns the narrow inline wrapper element. The
|
|
163
|
+
// Popup's alignX="end" then places the menu at that element's right
|
|
164
|
+
// edge instead of the content area's right edge. We correct this by
|
|
165
|
+
// resolving the nearest block-level ProseMirror ancestor and
|
|
166
|
+
// re-anchoring to its right edge.
|
|
167
|
+
var adjustedLeft = position.left;
|
|
168
|
+
if (blockAncestorDOM && blockAncestorDOM !== targetElement) {
|
|
169
|
+
var _position$left;
|
|
170
|
+
var blockRect = blockAncestorDOM.getBoundingClientRect();
|
|
171
|
+
// Shift left by the difference between the block's right edge and
|
|
172
|
+
// the inline target's right edge. This mirrors what alignX="end"
|
|
173
|
+
// would have computed if the target were the block element.
|
|
174
|
+
var leftDelta = blockRect.right - targetRect.right;
|
|
175
|
+
adjustedLeft = ((_position$left = position.left) !== null && _position$left !== void 0 ? _position$left : 0) + leftDelta;
|
|
176
|
+
}
|
|
100
177
|
return _objectSpread(_objectSpread({}, position), {}, {
|
|
101
|
-
top: adjustedTop
|
|
178
|
+
top: adjustedTop,
|
|
179
|
+
left: adjustedLeft
|
|
102
180
|
});
|
|
103
181
|
};
|
|
104
182
|
}
|
|
@@ -135,7 +213,12 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
|
|
|
135
213
|
var $pos = editorView.state.doc.resolve(lastContentPasted.pasteStartPos);
|
|
136
214
|
var pasteAncestorNodeNames = [];
|
|
137
215
|
for (var depth = $pos.depth; depth > 0; depth--) {
|
|
138
|
-
|
|
216
|
+
// Only include an ancestor if the entire pasted range is contained within it.
|
|
217
|
+
// This prevents nodes like 'heading' from being flagged as ancestors when the
|
|
218
|
+
// pasted content starts in a heading but extends beyond it (e.g. heading + paragraph).
|
|
219
|
+
if (lastContentPasted.pasteEndPos <= $pos.end(depth)) {
|
|
220
|
+
pasteAncestorNodeNames.push($pos.node(depth).type.name);
|
|
221
|
+
}
|
|
139
222
|
}
|
|
140
223
|
var legacyVisible = isToolbarVisible(editorView.state, lastContentPasted) && ((_lastContentPasted$te = (_lastContentPasted$te2 = lastContentPasted.text) === null || _lastContentPasted$te2 === void 0 ? void 0 : _lastContentPasted$te2.length) !== null && _lastContentPasted$te !== void 0 ? _lastContentPasted$te : 0) >= 100;
|
|
141
224
|
showToolbar(lastContentPasted, selectedOption, legacyVisible, pasteAncestorNodeNames)(editorView.state, editorView.dispatch);
|
|
@@ -219,7 +302,8 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
|
|
|
219
302
|
minPopupMargin: PASTE_MENU_GAP_HORIZONTAL,
|
|
220
303
|
zIndex: akEditorFloatingPanelZIndex,
|
|
221
304
|
alignX: "end",
|
|
222
|
-
alignY: "bottom"
|
|
305
|
+
alignY: "bottom"
|
|
306
|
+
/* eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) */,
|
|
223
307
|
offset: [PASTE_MENU_GAP_HORIZONTAL, 0],
|
|
224
308
|
onPositionCalculated: onPositionCalculated(editorView, pasteStartPos, pasteEndPos, target, effectiveScrollableElement),
|
|
225
309
|
handleClickOutside: handleClickOutside,
|
|
@@ -9,6 +9,11 @@ interface PasteActionsMenuProps {
|
|
|
9
9
|
mountTo?: HTMLElement;
|
|
10
10
|
scrollableElement?: HTMLElement;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Returns the DOM element at the given document position for use as a Popup anchor.
|
|
14
|
+
* For empty blocks (BR elements), returns the parent element to ensure correct positioning.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getTargetElement(editorView: EditorView, pos: number): HTMLElement | null;
|
|
12
17
|
/**
|
|
13
18
|
* Returns the position immediately after a table ancestor of `pos`, or
|
|
14
19
|
* `undefined` if not inside a table. Safe to cache per document version.
|
|
@@ -20,10 +25,20 @@ export declare function resolveTableAfterPos(editorView: EditorView, pos: number
|
|
|
20
25
|
*/
|
|
21
26
|
export declare function getVisualEndBottom(editorView: EditorView, pasteEndPos: number, tableAfterPos?: number): number;
|
|
22
27
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
28
|
+
* Finds the DOM element for the nearest block-level ProseMirror ancestor of
|
|
29
|
+
* the given document position. Uses ProseMirror's schema (`node.isBlock`)
|
|
30
|
+
* rather than CSS display properties, so the check is always in sync with the
|
|
31
|
+
* document model.
|
|
32
|
+
*
|
|
33
|
+
* Returns `null` if no block ancestor can be resolved to a DOM element.
|
|
34
|
+
*/
|
|
35
|
+
export declare function findBlockAncestorDOM(editorView: EditorView, pos: number): HTMLElement | null;
|
|
36
|
+
/**
|
|
37
|
+
* Adjusts the position of the paste menu so that:
|
|
38
|
+
*
|
|
39
|
+
* **Vertical:** The menu aligns with the top of the pasted content using the
|
|
40
|
+
* exact coordinates at the paste start position, and sticks to the top of the
|
|
41
|
+
* scroll container when the pasted content scrolls above the visible area.
|
|
27
42
|
*
|
|
28
43
|
* The Popup uses alignY="bottom", which positions the popup below the target
|
|
29
44
|
* element's bottom edge. This override:
|
|
@@ -34,6 +49,14 @@ export declare function getVisualEndBottom(editorView: EditorView, pasteEndPos:
|
|
|
34
49
|
* to the scroll container's top edge (sticky-top).
|
|
35
50
|
* 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
|
|
36
51
|
* above the visible area.
|
|
52
|
+
*
|
|
53
|
+
* **Horizontal:** When the target element is an inline element (e.g. a mark
|
|
54
|
+
* wrapper like `<strong>`, or an inline node like an emoji), the Popup's
|
|
55
|
+
* `alignX="end"` would place the menu at the right edge of that narrow
|
|
56
|
+
* element. This override resolves the nearest block-level ProseMirror
|
|
57
|
+
* ancestor (using `node.isBlock` from the document schema) and re-anchors
|
|
58
|
+
* the horizontal position to its right edge, so the menu consistently
|
|
59
|
+
* appears at the right side of the content area.
|
|
37
60
|
*/
|
|
38
61
|
export declare function onPositionCalculated(editorView: EditorView, pasteStartPos: number, pasteEndPos: number, targetElement: HTMLElement, scrollableElement?: HTMLElement | false): (position: {
|
|
39
62
|
bottom?: number;
|
|
@@ -9,6 +9,11 @@ interface PasteActionsMenuProps {
|
|
|
9
9
|
mountTo?: HTMLElement;
|
|
10
10
|
scrollableElement?: HTMLElement;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Returns the DOM element at the given document position for use as a Popup anchor.
|
|
14
|
+
* For empty blocks (BR elements), returns the parent element to ensure correct positioning.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getTargetElement(editorView: EditorView, pos: number): HTMLElement | null;
|
|
12
17
|
/**
|
|
13
18
|
* Returns the position immediately after a table ancestor of `pos`, or
|
|
14
19
|
* `undefined` if not inside a table. Safe to cache per document version.
|
|
@@ -20,10 +25,20 @@ export declare function resolveTableAfterPos(editorView: EditorView, pos: number
|
|
|
20
25
|
*/
|
|
21
26
|
export declare function getVisualEndBottom(editorView: EditorView, pasteEndPos: number, tableAfterPos?: number): number;
|
|
22
27
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
28
|
+
* Finds the DOM element for the nearest block-level ProseMirror ancestor of
|
|
29
|
+
* the given document position. Uses ProseMirror's schema (`node.isBlock`)
|
|
30
|
+
* rather than CSS display properties, so the check is always in sync with the
|
|
31
|
+
* document model.
|
|
32
|
+
*
|
|
33
|
+
* Returns `null` if no block ancestor can be resolved to a DOM element.
|
|
34
|
+
*/
|
|
35
|
+
export declare function findBlockAncestorDOM(editorView: EditorView, pos: number): HTMLElement | null;
|
|
36
|
+
/**
|
|
37
|
+
* Adjusts the position of the paste menu so that:
|
|
38
|
+
*
|
|
39
|
+
* **Vertical:** The menu aligns with the top of the pasted content using the
|
|
40
|
+
* exact coordinates at the paste start position, and sticks to the top of the
|
|
41
|
+
* scroll container when the pasted content scrolls above the visible area.
|
|
27
42
|
*
|
|
28
43
|
* The Popup uses alignY="bottom", which positions the popup below the target
|
|
29
44
|
* element's bottom edge. This override:
|
|
@@ -34,6 +49,14 @@ export declare function getVisualEndBottom(editorView: EditorView, pasteEndPos:
|
|
|
34
49
|
* to the scroll container's top edge (sticky-top).
|
|
35
50
|
* 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
|
|
36
51
|
* above the visible area.
|
|
52
|
+
*
|
|
53
|
+
* **Horizontal:** When the target element is an inline element (e.g. a mark
|
|
54
|
+
* wrapper like `<strong>`, or an inline node like an emoji), the Popup's
|
|
55
|
+
* `alignX="end"` would place the menu at the right edge of that narrow
|
|
56
|
+
* element. This override resolves the nearest block-level ProseMirror
|
|
57
|
+
* ancestor (using `node.isBlock` from the document schema) and re-anchors
|
|
58
|
+
* the horizontal position to its right edge, so the menu consistently
|
|
59
|
+
* appears at the right side of the content area.
|
|
37
60
|
*/
|
|
38
61
|
export declare function onPositionCalculated(editorView: EditorView, pasteStartPos: number, pasteEndPos: number, targetElement: HTMLElement, scrollableElement?: HTMLElement | false): (position: {
|
|
39
62
|
bottom?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-paste-options-toolbar",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.6",
|
|
4
4
|
"description": "Paste options toolbar for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"react-intl-next": "npm:react-intl@^5.18.1"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@atlaskit/editor-common": "^112.
|
|
51
|
+
"@atlaskit/editor-common": "^112.8.0",
|
|
52
52
|
"react": "^18.2.0",
|
|
53
53
|
"react-dom": "^18.2.0"
|
|
54
54
|
},
|