@atlaskit/editor-plugin-paste-options-toolbar 9.1.5 → 9.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cjs/pm-plugins/util/format-handlers.js +1 -1
  3. package/dist/cjs/ui/on-paste-actions-menu/PasteActionsMenu.js +133 -13
  4. package/dist/cjs/ui/on-paste-actions-menu/PasteActionsMenuContent.js +16 -13
  5. package/dist/cjs/ui/on-paste-actions-menu/PasteMenuComponents.js +45 -15
  6. package/dist/cjs/ui/on-paste-actions-menu/PasteOptionsDropdownButton.js +52 -0
  7. package/dist/es2019/pm-plugins/util/format-handlers.js +1 -1
  8. package/dist/es2019/ui/on-paste-actions-menu/PasteActionsMenu.js +131 -14
  9. package/dist/es2019/ui/on-paste-actions-menu/PasteActionsMenuContent.js +17 -14
  10. package/dist/es2019/ui/on-paste-actions-menu/PasteMenuComponents.js +45 -13
  11. package/dist/es2019/ui/on-paste-actions-menu/PasteOptionsDropdownButton.js +47 -0
  12. package/dist/esm/pm-plugins/util/format-handlers.js +1 -1
  13. package/dist/esm/ui/on-paste-actions-menu/PasteActionsMenu.js +132 -14
  14. package/dist/esm/ui/on-paste-actions-menu/PasteActionsMenuContent.js +17 -14
  15. package/dist/esm/ui/on-paste-actions-menu/PasteMenuComponents.js +45 -15
  16. package/dist/esm/ui/on-paste-actions-menu/PasteOptionsDropdownButton.js +46 -0
  17. package/dist/types/ui/on-paste-actions-menu/PasteActionsMenu.d.ts +38 -4
  18. package/dist/types/ui/on-paste-actions-menu/PasteActionsMenuContent.d.ts +2 -1
  19. package/dist/types/ui/on-paste-actions-menu/PasteOptionsDropdownButton.d.ts +19 -0
  20. package/dist/types-ts4.5/ui/on-paste-actions-menu/PasteActionsMenu.d.ts +38 -4
  21. package/dist/types-ts4.5/ui/on-paste-actions-menu/PasteActionsMenuContent.d.ts +2 -1
  22. package/dist/types-ts4.5/ui/on-paste-actions-menu/PasteOptionsDropdownButton.d.ts +19 -0
  23. package/package.json +6 -5
@@ -4,7 +4,7 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
4
4
  import React, { useCallback, useEffect, useRef } from 'react';
5
5
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
6
6
  import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
7
- import { EditorToolbarProvider, PASTE_MENU } from '@atlaskit/editor-common/toolbar';
7
+ import { AI_PASTE_MENU_SECTION, EditorToolbarProvider, PASTE_MENU } from '@atlaskit/editor-common/toolbar';
8
8
  import { findOverflowScrollParent, Popup } from '@atlaskit/editor-common/ui';
9
9
  import { withReactEditorViewOuterListeners } from '@atlaskit/editor-common/ui-react';
10
10
  import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
@@ -69,10 +69,76 @@ export function getVisualEndBottom(editorView, pasteEndPos, tableAfterPos) {
69
69
  }
70
70
 
71
71
  /**
72
- * Adjusts the vertical position of the paste menu to align with the top of the
73
- * pasted content using the exact coordinates at the paste start position,
74
- * and sticks the menu to the top of the scroll container when the pasted
75
- * content scrolls above the visible area.
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
+ * Positions the paste menu inline, immediately to the right of the cursor
109
+ * at the paste end position, vertically centered with the line.
110
+ * Used for short pastes without AI actions.
111
+ */
112
+ export function onInlinePositionCalculated(editorView, pasteEndPos, targetElement, popupContentRef) {
113
+ return function (position) {
114
+ var _popupContentRef$curr, _popupContentRef$curr2, _position$top, _position$left;
115
+ var endCoords = editorView.coordsAtPos(pasteEndPos);
116
+ var targetRect = targetElement.getBoundingClientRect();
117
+
118
+ // Vertical: center the menu with the line at the paste end position.
119
+ var lineHeight = endCoords.bottom - endCoords.top;
120
+ var lineMidpoint = endCoords.top + lineHeight / 2;
121
+ var menuHeight = (_popupContentRef$curr = (_popupContentRef$curr2 = popupContentRef.current) === null || _popupContentRef$curr2 === void 0 ? void 0 : _popupContentRef$curr2.getBoundingClientRect().height) !== null && _popupContentRef$curr !== void 0 ? _popupContentRef$curr : lineHeight;
122
+ var menuTop = lineMidpoint - menuHeight / 2;
123
+ var topDelta = menuTop - (targetRect.top + targetRect.height);
124
+ var adjustedTop = ((_position$top = position.top) !== null && _position$top !== void 0 ? _position$top : 0) + topDelta;
125
+
126
+ // Horizontal: position to the right of the cursor
127
+ var leftDelta = endCoords.right - targetRect.right;
128
+ var adjustedLeft = ((_position$left = position.left) !== null && _position$left !== void 0 ? _position$left : 0) + leftDelta;
129
+ return _objectSpread(_objectSpread({}, position), {}, {
130
+ top: adjustedTop,
131
+ left: adjustedLeft
132
+ });
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Adjusts the position of the paste menu so that:
138
+ *
139
+ * **Vertical:** The menu aligns with the top of the pasted content using the
140
+ * exact coordinates at the paste start position, and sticks to the top of the
141
+ * scroll container when the pasted content scrolls above the visible area.
76
142
  *
77
143
  * The Popup uses alignY="bottom", which positions the popup below the target
78
144
  * element's bottom edge. This override:
@@ -83,21 +149,31 @@ export function getVisualEndBottom(editorView, pasteEndPos, tableAfterPos) {
83
149
  * to the scroll container's top edge (sticky-top).
84
150
  * 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
85
151
  * above the visible area.
152
+ *
153
+ * **Horizontal:** When the target element is an inline element (e.g. a mark
154
+ * wrapper like `<strong>`, or an inline node like an emoji), the Popup's
155
+ * `alignX="end"` would place the menu at the right edge of that narrow
156
+ * element. This override resolves the nearest block-level ProseMirror
157
+ * ancestor (using `node.isBlock` from the document schema) and re-anchors
158
+ * the horizontal position to its right edge, so the menu consistently
159
+ * appears at the right side of the content area.
86
160
  */
87
161
  export function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, targetElement, scrollableElement) {
88
162
  // Pre-compute once per render to avoid doc.resolve() on every scroll frame.
89
163
  var tableAfterPos = resolveTableAfterPos(editorView, pasteEndPos);
164
+ var blockAncestorDOM = findBlockAncestorDOM(editorView, pasteStartPos);
90
165
  return function (position) {
91
- var _position$top;
166
+ var _position$top2;
92
167
  var startCoords = editorView.coordsAtPos(pasteStartPos);
93
168
  var endBottom = getVisualEndBottom(editorView, pasteEndPos, tableAfterPos);
94
169
  var targetRect = targetElement.getBoundingClientRect();
95
170
 
171
+ // ── Vertical adjustment ──────────────────────────────────────────
96
172
  // The Popup places the menu at the target's bottom edge by default.
97
173
  // We shift it up so it aligns with the paste start position.
98
174
  // Both coordinates are in viewport space, so the delta is offset-parent agnostic.
99
175
  var topDelta = startCoords.top - (targetRect.top + targetRect.height);
100
- var adjustedTop = ((_position$top = position.top) !== null && _position$top !== void 0 ? _position$top : 0) + topDelta;
176
+ var adjustedTop = ((_position$top2 = position.top) !== null && _position$top2 !== void 0 ? _position$top2 : 0) + topDelta;
101
177
 
102
178
  // Sticky-top: clamp to the scroll container's top edge when the paste
103
179
  // start has scrolled above the visible area, but only while some pasted
@@ -108,8 +184,28 @@ export function onPositionCalculated(editorView, pasteStartPos, pasteEndPos, tar
108
184
  adjustedTop += scrollContainerTop - startCoords.top + PASTE_MENU_GAP_TOP;
109
185
  }
110
186
  }
187
+
188
+ // ── Horizontal adjustment ────────────────────────────────────────
189
+ // When pasted content starts with a mark (bold, italic, link …) or
190
+ // an inline node (emoji, smart link, inline image …),
191
+ // findDomRefAtPos returns the narrow inline wrapper element. The
192
+ // Popup's alignX="end" then places the menu at that element's right
193
+ // edge instead of the content area's right edge. We correct this by
194
+ // resolving the nearest block-level ProseMirror ancestor and
195
+ // re-anchoring to its right edge.
196
+ var adjustedLeft = position.left;
197
+ if (blockAncestorDOM && blockAncestorDOM !== targetElement) {
198
+ var _position$left2;
199
+ var blockRect = blockAncestorDOM.getBoundingClientRect();
200
+ // Shift left by the difference between the block's right edge and
201
+ // the inline target's right edge. This mirrors what alignX="end"
202
+ // would have computed if the target were the block element.
203
+ var leftDelta = blockRect.right - targetRect.right;
204
+ adjustedLeft = ((_position$left2 = position.left) !== null && _position$left2 !== void 0 ? _position$left2 : 0) + leftDelta;
205
+ }
111
206
  return _objectSpread(_objectSpread({}, position), {}, {
112
- top: adjustedTop
207
+ top: adjustedTop,
208
+ left: adjustedLeft
113
209
  });
114
210
  };
115
211
  }
@@ -129,8 +225,8 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
129
225
  }),
130
226
  lastContentPasted = _useSharedPluginState.lastContentPasted;
131
227
  var prevShowToolbarRef = useRef(false);
228
+ var popupContentRef = useRef(null);
132
229
  useEffect(function () {
133
- var _lastContentPasted$te, _lastContentPasted$te2;
134
230
  if (!lastContentPasted) {
135
231
  hideToolbar()(editorView.state, editorView.dispatch);
136
232
  return;
@@ -153,7 +249,7 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
153
249
  pasteAncestorNodeNames.push($pos.node(depth).type.name);
154
250
  }
155
251
  }
156
- 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;
252
+ var legacyVisible = isToolbarVisible(editorView.state, lastContentPasted);
157
253
  showToolbar(lastContentPasted, selectedOption, legacyVisible, pasteAncestorNodeNames)(editorView.state, editorView.dispatch);
158
254
  }, [lastContentPasted, editorView]);
159
255
  var _useSharedPluginState2 = useSharedPluginStateWithSelector(api, ['pasteOptionsToolbarPlugin'], function (states) {
@@ -217,16 +313,36 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
217
313
  var effectiveScrollableElement = overflowScrollParent || scrollableElement;
218
314
  var pasteMenuComponents = (_api$uiControlRegistr3 = api === null || api === void 0 || (_api$uiControlRegistr4 = api.uiControlRegistry) === null || _api$uiControlRegistr4 === void 0 ? void 0 : _api$uiControlRegistr4.actions.getComponents(PASTE_MENU.key)) !== null && _api$uiControlRegistr3 !== void 0 ? _api$uiControlRegistr3 : [];
219
315
  var anyComponentVisible = hasVisibleButton(pasteMenuComponents);
316
+
317
+ // Two positioning modes:
318
+ // 1. Inline: no AI actions visible — menu appears to the right of the cursor,
319
+ // vertically centered with the text line.
320
+ // 2. Block-anchored: AI actions are visible — menu appears at the right edge
321
+ // of the content block, aligned with paste start.
322
+ var hasVisibleAiActions = getVisibleKeys(
323
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-computations-in-render -- pasteMenuComponents changes by reference each render; filter is small (< 10 items)
324
+ pasteMenuComponents.filter(function (c) {
325
+ var _c$parents;
326
+ return c.type === 'menu-item' && ((_c$parents = c.parents) === null || _c$parents === void 0 ? void 0 : _c$parents.some(function (p) {
327
+ return p.key === AI_PASTE_MENU_SECTION.key;
328
+ }));
329
+ }), ['menu-item']).length > 0;
330
+ var useInlinePosition = !hasVisibleAiActions;
220
331
  if (!isToolbarShown) {
221
332
  return null;
222
333
  }
223
334
  if (!anyComponentVisible) {
224
335
  return null;
225
336
  }
226
- var target = getTargetElement(editorView, pasteStartPos);
337
+ var target = getTargetElement(editorView, useInlinePosition ? pasteEndPos : pasteStartPos);
227
338
  if (!target) {
228
339
  return null;
229
340
  }
341
+
342
+ // Choose positioning strategy based on whether the menu appears inline
343
+ // (right of cursor for short pastes) or anchored to the block ancestor
344
+ // (right side of content area for longer pastes / AI actions).
345
+ var positionCalculator = useInlinePosition ? onInlinePositionCalculated(editorView, pasteEndPos, target, popupContentRef) : onPositionCalculated(editorView, pasteStartPos, pasteEndPos, target, effectiveScrollableElement);
230
346
  return /*#__PURE__*/React.createElement(PopupWithListeners, {
231
347
  target: target,
232
348
  mountTo: mountTo,
@@ -235,9 +351,10 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
235
351
  minPopupMargin: PASTE_MENU_GAP_HORIZONTAL,
236
352
  zIndex: akEditorFloatingPanelZIndex,
237
353
  alignX: "end",
238
- alignY: "bottom",
354
+ alignY: useInlinePosition ? 'top' : 'bottom'
355
+ /* eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) */,
239
356
  offset: [PASTE_MENU_GAP_HORIZONTAL, 0],
240
- onPositionCalculated: onPositionCalculated(editorView, pasteStartPos, pasteEndPos, target, effectiveScrollableElement),
357
+ onPositionCalculated: positionCalculator,
241
358
  handleClickOutside: handleClickOutside,
242
359
  handleEscapeKeydown: handleDismiss
243
360
  }, /*#__PURE__*/React.createElement(EditorToolbarProvider, {
@@ -248,6 +365,7 @@ export var PasteActionsMenu = function PasteActionsMenu(_ref) {
248
365
  }, /*#__PURE__*/React.createElement(PasteActionsMenuContent, {
249
366
  onMouseDown: preventEditorFocusLoss,
250
367
  onMouseEnter: handleMouseEnter,
251
- components: pasteMenuComponents
368
+ components: pasteMenuComponents,
369
+ contentRef: popupContentRef
252
370
  }))));
253
371
  };
@@ -1,34 +1,37 @@
1
1
  /* PasteActionsMenuContent.tsx generated by @compiled/babel-plugin v0.39.1 */
2
2
  import "./PasteActionsMenuContent.compiled.css";
3
3
  import { ax, ix } from "@compiled/react/runtime";
4
- import React, { useContext } from 'react';
5
- import { useIntl } from 'react-intl-next';
6
- import { pasteOptionsToolbarMessages as messages } from '@atlaskit/editor-common/messages';
4
+ import React, { useCallback, useContext } from 'react';
5
+ import { PASTE_MENU } from '@atlaskit/editor-common/toolbar';
7
6
  import { OutsideClickTargetRefContext } from '@atlaskit/editor-common/ui-react';
8
- import { ToolbarDropdownItemSection } from '@atlaskit/editor-toolbar';
9
7
  import { SurfaceRenderer } from '@atlaskit/editor-ui-control-model';
10
8
  import { Box } from '@atlaskit/primitives/compiled';
11
9
  var styles = {
12
10
  container: "_2rko12b0 _bfhk1bhr _16qs130s"
13
11
  };
12
+ var pasteMenuSurface = {
13
+ type: PASTE_MENU.type,
14
+ key: PASTE_MENU.key
15
+ };
14
16
  export var PasteActionsMenuContent = function PasteActionsMenuContent(_ref) {
15
17
  var onMouseDown = _ref.onMouseDown,
16
18
  onMouseEnter = _ref.onMouseEnter,
17
- components = _ref.components;
19
+ components = _ref.components,
20
+ contentRef = _ref.contentRef;
18
21
  var setOutsideClickTargetRef = useContext(OutsideClickTargetRefContext);
19
- var intl = useIntl();
22
+ var mergedRef = useCallback(function (node) {
23
+ setOutsideClickTargetRef === null || setOutsideClickTargetRef === void 0 || setOutsideClickTargetRef(node);
24
+ if (contentRef) {
25
+ contentRef.current = node;
26
+ }
27
+ }, [setOutsideClickTargetRef, contentRef]);
20
28
  return /*#__PURE__*/React.createElement(Box, {
21
- ref: setOutsideClickTargetRef,
29
+ ref: mergedRef,
22
30
  xcss: styles.container,
23
31
  onMouseDown: onMouseDown,
24
32
  onMouseEnter: onMouseEnter
25
- }, /*#__PURE__*/React.createElement(ToolbarDropdownItemSection, {
26
- title: intl.formatMessage(messages.pasteMenuActionsTitle)
27
33
  }, /*#__PURE__*/React.createElement(SurfaceRenderer, {
28
- surface: {
29
- type: 'menu',
30
- key: 'paste-menu'
31
- },
34
+ surface: pasteMenuSurface,
32
35
  components: components
33
- })));
36
+ }));
34
37
  };
@@ -8,12 +8,14 @@ import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'
8
8
  import { pasteOptionsToolbarMessages as messages } from '@atlaskit/editor-common/messages';
9
9
  import { useEditorToolbar, PASTE_MENU, PASTE_MENU_SECTION, PASTE_NESTED_MENU, PASTE_MENU_NESTED_SECTION, PASTE_RICH_TEXT_MENU_ITEM, PASTE_MARKDOWN_MENU_ITEM, PASTE_PLAIN_TEXT_MENU_ITEM, PASTE_MENU_RANK, PASTE_MENU_SECTION_RANK, PASTE_NESTED_MENU_RANK, PASTE_MENU_NESTED_SECTION_RANK, AI_PASTE_MENU_SECTION } from '@atlaskit/editor-common/toolbar';
10
10
  import { ToolbarDropdownItem, ToolbarDropdownItemSection, ToolbarNestedDropdownMenu } from '@atlaskit/editor-toolbar';
11
+ import ChevronDownIcon from '@atlaskit/icon/core/chevron-down';
11
12
  import ChevronRightIcon from '@atlaskit/icon/core/chevron-right';
12
13
  import ClipboardIcon from '@atlaskit/icon/core/clipboard';
13
14
  import { Box } from '@atlaskit/primitives/compiled';
14
15
  import { changeToMarkdownWithAnalytics, changeToPlainTextWithAnalytics, changeToRichTextWithAnalytics } from '../../editor-commands/commands';
15
16
  import { ToolbarDropdownOption } from '../../types/types';
16
17
  import { getVisibleKeys } from './hasVisibleButton';
18
+ import { PasteOptionsDropdownButton } from './PasteOptionsDropdownButton';
17
19
  var nestedMenuStyles = {
18
20
  narrowSection: "_10gv1lit"
19
21
  };
@@ -91,21 +93,49 @@ var PasteMenuItem = function PasteMenuItem(_ref) {
91
93
  }, displayLabel);
92
94
  };
93
95
  var PasteOptionsNestedMenu = function PasteOptionsNestedMenu(_ref2) {
94
- var children = _ref2.children;
96
+ var children = _ref2.children,
97
+ hasVisibleAiActions = _ref2.hasVisibleAiActions;
95
98
  var intl = useIntl();
99
+ var label = intl.formatMessage(messages.pasteMenuActionsPasteAs);
100
+ if (!hasVisibleAiActions) {
101
+ return /*#__PURE__*/React.createElement(PasteOptionsDropdownButton, {
102
+ elemBefore: /*#__PURE__*/React.createElement(ClipboardIcon, {
103
+ size: "small",
104
+ label: ""
105
+ }),
106
+ elemAfter: /*#__PURE__*/React.createElement(ChevronDownIcon, {
107
+ size: "small",
108
+ label: ""
109
+ }),
110
+ label: label,
111
+ testId: "paste-options-nested-menu",
112
+ tooltipContent: label
113
+ }, children);
114
+ }
96
115
  return /*#__PURE__*/React.createElement(ToolbarNestedDropdownMenu, {
97
116
  elemBefore: /*#__PURE__*/React.createElement(ClipboardIcon, {
98
117
  size: "small",
99
- label: intl.formatMessage(messages.pasteOptions)
118
+ label: label
100
119
  }),
101
120
  elemAfter: /*#__PURE__*/React.createElement(ChevronRightIcon, {
102
121
  size: "small",
103
- label: intl.formatMessage(messages.pasteOptions)
122
+ label: ""
104
123
  }),
105
124
  testId: "paste-options-nested-menu",
106
- text: intl.formatMessage(messages.pasteOptions)
125
+ text: label
107
126
  }, children);
108
127
  };
128
+ var getHasVisibleAiActions = function getHasVisibleAiActions(api) {
129
+ var _api$uiControlRegistr, _api$uiControlRegistr2;
130
+ var allComponents = (_api$uiControlRegistr = api === null || api === void 0 || (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(PASTE_MENU.key)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
131
+ var aiMenuItems = allComponents.filter(function (c) {
132
+ var _c$parents;
133
+ return c.type === 'menu-item' && ((_c$parents = c.parents) === null || _c$parents === void 0 ? void 0 : _c$parents.some(function (p) {
134
+ return p.key === AI_PASTE_MENU_SECTION.key;
135
+ }));
136
+ });
137
+ return getVisibleKeys(aiMenuItems, ['menu-item']).length > 0;
138
+ };
109
139
  export var getPasteMenuComponents = function getPasteMenuComponents(_ref3) {
110
140
  var api = _ref3.api;
111
141
  return [{
@@ -125,17 +155,14 @@ export var getPasteMenuComponents = function getPasteMenuComponents(_ref3) {
125
155
  return !((_pluginState$showLega = pluginState === null || pluginState === void 0 ? void 0 : pluginState.showLegacyOptions) !== null && _pluginState$showLega !== void 0 ? _pluginState$showLega : false);
126
156
  },
127
157
  component: function component(props) {
128
- var _api$uiControlRegistr, _api$uiControlRegistr2;
129
- var allComponents = (_api$uiControlRegistr = api === null || api === void 0 || (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(PASTE_MENU.key)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
130
- var aiMenuItems = allComponents.filter(function (c) {
131
- var _c$parents;
132
- return c.type === 'menu-item' && ((_c$parents = c.parents) === null || _c$parents === void 0 ? void 0 : _c$parents.some(function (p) {
133
- return p.key === AI_PASTE_MENU_SECTION.key;
134
- }));
135
- });
136
- var hasVisibleAiActions = getVisibleKeys(aiMenuItems, ['menu-item']).length > 0;
158
+ var hasVisibleAiActions = getHasVisibleAiActions(api);
159
+ if (!hasVisibleAiActions) {
160
+ return /*#__PURE__*/React.createElement(Box, {
161
+ padding: "space.050"
162
+ }, props.children);
163
+ }
137
164
  return /*#__PURE__*/React.createElement(ToolbarDropdownItemSection, {
138
- hasSeparator: hasVisibleAiActions
165
+ hasSeparator: true
139
166
  }, props.children);
140
167
  }
141
168
  }, {
@@ -147,7 +174,10 @@ export var getPasteMenuComponents = function getPasteMenuComponents(_ref3) {
147
174
  rank: PASTE_MENU_SECTION_RANK[PASTE_NESTED_MENU.key]
148
175
  }],
149
176
  component: function component(props) {
150
- return /*#__PURE__*/React.createElement(PasteOptionsNestedMenu, null, props.children);
177
+ var hasVisibleAiActions = getHasVisibleAiActions(api);
178
+ return /*#__PURE__*/React.createElement(PasteOptionsNestedMenu, {
179
+ hasVisibleAiActions: hasVisibleAiActions
180
+ }, props.children);
151
181
  }
152
182
  }, {
153
183
  type: PASTE_MENU_NESTED_SECTION.type,
@@ -0,0 +1,46 @@
1
+ /* PasteOptionsDropdownButton.tsx generated by @compiled/babel-plugin v0.39.1 */
2
+ import { ax, ix } from "@compiled/react/runtime";
3
+ import React, { useCallback } from 'react';
4
+
5
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
6
+
7
+ import DropdownMenu from '@atlaskit/dropdown-menu';
8
+ import { ToolbarButton, ToolbarTooltip } from '@atlaskit/editor-toolbar';
9
+
10
+ /**
11
+ * A compact dropdown button for paste options, styled like floating toolbar buttons.
12
+ * Renders as a ToolbarButton with an icon and dropdown caret that opens a
13
+ * dropdown menu below. Used when AI actions are not visible and the paste
14
+ * options menu is the only content.
15
+ */
16
+ export var PasteOptionsDropdownButton = function PasteOptionsDropdownButton(_ref) {
17
+ var children = _ref.children,
18
+ elemBefore = _ref.elemBefore,
19
+ elemAfter = _ref.elemAfter,
20
+ label = _ref.label,
21
+ testId = _ref.testId,
22
+ tooltipContent = _ref.tooltipContent;
23
+ var trigger = useCallback(function (triggerProps) {
24
+ var button = /*#__PURE__*/React.createElement(ToolbarButton, {
25
+ ref: triggerProps.triggerRef,
26
+ isSelected: triggerProps.isSelected,
27
+ "aria-expanded": triggerProps['aria-expanded'],
28
+ "aria-haspopup": triggerProps['aria-haspopup'],
29
+ onClick: triggerProps.onClick,
30
+ testId: testId,
31
+ iconBefore: elemBefore,
32
+ label: label
33
+ }, elemAfter);
34
+ if (tooltipContent) {
35
+ return /*#__PURE__*/React.createElement(ToolbarTooltip, {
36
+ content: tooltipContent,
37
+ position: "top"
38
+ }, button);
39
+ }
40
+ return button;
41
+ }, [testId, elemBefore, elemAfter, label, tooltipContent]);
42
+ return /*#__PURE__*/React.createElement(DropdownMenu, {
43
+ placement: "bottom-start",
44
+ trigger: trigger
45
+ }, children);
46
+ };
@@ -25,10 +25,36 @@ export declare function resolveTableAfterPos(editorView: EditorView, pos: number
25
25
  */
26
26
  export declare function getVisualEndBottom(editorView: EditorView, pasteEndPos: number, tableAfterPos?: number): number;
27
27
  /**
28
- * Adjusts the vertical position of the paste menu to align with the top of the
29
- * pasted content using the exact coordinates at the paste start position,
30
- * and sticks the menu to the top of the scroll container when the pasted
31
- * content scrolls above the visible area.
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
+ * Positions the paste menu inline, immediately to the right of the cursor
38
+ * at the paste end position, vertically centered with the line.
39
+ * Used for short pastes without AI actions.
40
+ */
41
+ export declare function onInlinePositionCalculated(editorView: EditorView, pasteEndPos: number, targetElement: HTMLElement, popupContentRef: React.RefObject<HTMLDivElement | null>): (position: {
42
+ bottom?: number;
43
+ left?: number;
44
+ right?: number;
45
+ top?: number;
46
+ }) => {
47
+ bottom?: number;
48
+ left?: number;
49
+ right?: number;
50
+ top: number;
51
+ };
52
+ /**
53
+ * Adjusts the position of the paste menu so that:
54
+ *
55
+ * **Vertical:** The menu aligns with the top of the pasted content using the
56
+ * exact coordinates at the paste start position, and sticks to the top of the
57
+ * scroll container when the pasted content scrolls above the visible area.
32
58
  *
33
59
  * The Popup uses alignY="bottom", which positions the popup below the target
34
60
  * element's bottom edge. This override:
@@ -39,6 +65,14 @@ export declare function getVisualEndBottom(editorView: EditorView, pasteEndPos:
39
65
  * to the scroll container's top edge (sticky-top).
40
66
  * 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
41
67
  * above the visible area.
68
+ *
69
+ * **Horizontal:** When the target element is an inline element (e.g. a mark
70
+ * wrapper like `<strong>`, or an inline node like an emoji), the Popup's
71
+ * `alignX="end"` would place the menu at the right edge of that narrow
72
+ * element. This override resolves the nearest block-level ProseMirror
73
+ * ancestor (using `node.isBlock` from the document schema) and re-anchors
74
+ * the horizontal position to its right edge, so the menu consistently
75
+ * appears at the right side of the content area.
42
76
  */
43
77
  export declare function onPositionCalculated(editorView: EditorView, pasteStartPos: number, pasteEndPos: number, targetElement: HTMLElement, scrollableElement?: HTMLElement | false): (position: {
44
78
  bottom?: number;
@@ -2,8 +2,9 @@ import React from 'react';
2
2
  import type { RegisterComponent } from '@atlaskit/editor-ui-control-model';
3
3
  interface PasteActionsMenuContentProps {
4
4
  components: RegisterComponent[];
5
+ contentRef?: React.RefObject<HTMLDivElement | null>;
5
6
  onMouseDown: (e: React.MouseEvent) => void;
6
7
  onMouseEnter: () => void;
7
8
  }
8
- export declare const PasteActionsMenuContent: ({ onMouseDown, onMouseEnter, components, }: PasteActionsMenuContentProps) => React.JSX.Element;
9
+ export declare const PasteActionsMenuContent: ({ onMouseDown, onMouseEnter, components, contentRef, }: PasteActionsMenuContentProps) => React.JSX.Element;
9
10
  export {};
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ */
5
+ import React from 'react';
6
+ /**
7
+ * A compact dropdown button for paste options, styled like floating toolbar buttons.
8
+ * Renders as a ToolbarButton with an icon and dropdown caret that opens a
9
+ * dropdown menu below. Used when AI actions are not visible and the paste
10
+ * options menu is the only content.
11
+ */
12
+ export declare const PasteOptionsDropdownButton: ({ children, elemBefore, elemAfter, label, testId, tooltipContent, }: {
13
+ children?: React.ReactNode;
14
+ elemAfter: React.ReactNode;
15
+ elemBefore: React.ReactNode;
16
+ label: string;
17
+ testId?: string;
18
+ tooltipContent?: string;
19
+ }) => JSX.Element;
@@ -25,10 +25,36 @@ export declare function resolveTableAfterPos(editorView: EditorView, pos: number
25
25
  */
26
26
  export declare function getVisualEndBottom(editorView: EditorView, pasteEndPos: number, tableAfterPos?: number): number;
27
27
  /**
28
- * Adjusts the vertical position of the paste menu to align with the top of the
29
- * pasted content using the exact coordinates at the paste start position,
30
- * and sticks the menu to the top of the scroll container when the pasted
31
- * content scrolls above the visible area.
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
+ * Positions the paste menu inline, immediately to the right of the cursor
38
+ * at the paste end position, vertically centered with the line.
39
+ * Used for short pastes without AI actions.
40
+ */
41
+ export declare function onInlinePositionCalculated(editorView: EditorView, pasteEndPos: number, targetElement: HTMLElement, popupContentRef: React.RefObject<HTMLDivElement | null>): (position: {
42
+ bottom?: number;
43
+ left?: number;
44
+ right?: number;
45
+ top?: number;
46
+ }) => {
47
+ bottom?: number;
48
+ left?: number;
49
+ right?: number;
50
+ top: number;
51
+ };
52
+ /**
53
+ * Adjusts the position of the paste menu so that:
54
+ *
55
+ * **Vertical:** The menu aligns with the top of the pasted content using the
56
+ * exact coordinates at the paste start position, and sticks to the top of the
57
+ * scroll container when the pasted content scrolls above the visible area.
32
58
  *
33
59
  * The Popup uses alignY="bottom", which positions the popup below the target
34
60
  * element's bottom edge. This override:
@@ -39,6 +65,14 @@ export declare function getVisualEndBottom(editorView: EditorView, pasteEndPos:
39
65
  * to the scroll container's top edge (sticky-top).
40
66
  * 3. Stops sticking once the entire pasted range (pasteEndPos) has scrolled
41
67
  * above the visible area.
68
+ *
69
+ * **Horizontal:** When the target element is an inline element (e.g. a mark
70
+ * wrapper like `<strong>`, or an inline node like an emoji), the Popup's
71
+ * `alignX="end"` would place the menu at the right edge of that narrow
72
+ * element. This override resolves the nearest block-level ProseMirror
73
+ * ancestor (using `node.isBlock` from the document schema) and re-anchors
74
+ * the horizontal position to its right edge, so the menu consistently
75
+ * appears at the right side of the content area.
42
76
  */
43
77
  export declare function onPositionCalculated(editorView: EditorView, pasteStartPos: number, pasteEndPos: number, targetElement: HTMLElement, scrollableElement?: HTMLElement | false): (position: {
44
78
  bottom?: number;
@@ -2,8 +2,9 @@ import React from 'react';
2
2
  import type { RegisterComponent } from '@atlaskit/editor-ui-control-model';
3
3
  interface PasteActionsMenuContentProps {
4
4
  components: RegisterComponent[];
5
+ contentRef?: React.RefObject<HTMLDivElement | null>;
5
6
  onMouseDown: (e: React.MouseEvent) => void;
6
7
  onMouseEnter: () => void;
7
8
  }
8
- export declare const PasteActionsMenuContent: ({ onMouseDown, onMouseEnter, components, }: PasteActionsMenuContentProps) => React.JSX.Element;
9
+ export declare const PasteActionsMenuContent: ({ onMouseDown, onMouseEnter, components, contentRef, }: PasteActionsMenuContentProps) => React.JSX.Element;
9
10
  export {};
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ */
5
+ import React from 'react';
6
+ /**
7
+ * A compact dropdown button for paste options, styled like floating toolbar buttons.
8
+ * Renders as a ToolbarButton with an icon and dropdown caret that opens a
9
+ * dropdown menu below. Used when AI actions are not visible and the paste
10
+ * options menu is the only content.
11
+ */
12
+ export declare const PasteOptionsDropdownButton: ({ children, elemBefore, elemAfter, label, testId, tooltipContent, }: {
13
+ children?: React.ReactNode;
14
+ elemAfter: React.ReactNode;
15
+ elemBefore: React.ReactNode;
16
+ label: string;
17
+ testId?: string;
18
+ tooltipContent?: string;
19
+ }) => JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-paste-options-toolbar",
3
- "version": "9.1.5",
3
+ "version": "9.1.7",
4
4
  "description": "Paste options toolbar for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -30,6 +30,7 @@
30
30
  "atlaskit:src": "src/index.ts",
31
31
  "dependencies": {
32
32
  "@atlaskit/css": "^0.19.0",
33
+ "@atlaskit/dropdown-menu": "^16.8.0",
33
34
  "@atlaskit/editor-markdown-transformer": "^5.20.0",
34
35
  "@atlaskit/editor-plugin-analytics": "^8.0.0",
35
36
  "@atlaskit/editor-plugin-paste": "^9.0.0",
@@ -38,17 +39,17 @@
38
39
  "@atlaskit/editor-shared-styles": "^3.10.0",
39
40
  "@atlaskit/editor-toolbar": "^0.20.0",
40
41
  "@atlaskit/editor-ui-control-model": "^1.1.0",
41
- "@atlaskit/icon": "^33.0.0",
42
+ "@atlaskit/icon": "^33.1.0",
42
43
  "@atlaskit/platform-feature-flags": "^1.1.0",
43
- "@atlaskit/primitives": "^18.0.0",
44
- "@atlaskit/tokens": "^11.1.0",
44
+ "@atlaskit/primitives": "^18.1.0",
45
+ "@atlaskit/tokens": "^11.3.0",
45
46
  "@babel/runtime": "^7.0.0",
46
47
  "@compiled/react": "^0.20.0",
47
48
  "@emotion/react": "^11.7.1",
48
49
  "react-intl-next": "npm:react-intl@^5.18.1"
49
50
  },
50
51
  "peerDependencies": {
51
- "@atlaskit/editor-common": "^112.6.0",
52
+ "@atlaskit/editor-common": "^112.11.0",
52
53
  "react": "^18.2.0",
53
54
  "react-dom": "^18.2.0"
54
55
  },