@atlaskit/editor-plugin-block-controls 8.4.3 → 8.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Remix block control: positioned at the right edge of the block.
3
+ * Uses anchor(anchorName end) or getRightPositionForRootElement (same coordinate system
4
+ * as left controls). The widget span has no position:relative so position:absolute
5
+ * uses the same containing block as the node.
6
+ */
7
+ /* @jsxRuntime classic */
8
+ /**
9
+ * @jsxRuntime classic
10
+ * @jsx jsx
11
+ */
12
+
13
+ import React, { useCallback, useEffect, useState } from 'react';
14
+
15
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
16
+ import { css, jsx } from '@emotion/react';
17
+ import { bind } from 'bind-event-listener';
18
+ import { IconButton } from '@atlaskit/button/new';
19
+ import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
20
+ import RandomizeIcon from '@atlaskit/icon-lab/core/randomize';
21
+ import { getTopPosition } from '../pm-plugins/utils/drag-handle-positions';
22
+ import { getRightPositionForRootElement } from '../pm-plugins/utils/widget-positions';
23
+ import { REMIX_BUTTON_DIMENSIONS, REMIX_BUTTON_RIGHT_OFFSET, rootElementGap, topPositionAdjustment } from './consts';
24
+ import { refreshAnchorName } from './utils/anchor-name';
25
+ import { getAnchorAttrName } from './utils/dom-attr-name';
26
+ import { VisibilityContainer } from './visibility-container';
27
+ const containerBaseStyles = css({
28
+ position: 'absolute',
29
+ zIndex: 100,
30
+ display: 'flex',
31
+ alignItems: 'center',
32
+ justifyContent: 'center'
33
+ });
34
+ export const RemixButton = ({
35
+ view,
36
+ api,
37
+ getPos,
38
+ anchorName,
39
+ rootAnchorName,
40
+ rootNodeType
41
+ }) => {
42
+ const {
43
+ macroInteractionUpdates
44
+ } = useSharedPluginStateWithSelector(api, ['featureFlags'], states => {
45
+ var _states$featureFlagsS;
46
+ return {
47
+ macroInteractionUpdates: (_states$featureFlagsS = states.featureFlagsState) === null || _states$featureFlagsS === void 0 ? void 0 : _states$featureFlagsS.macroInteractionUpdates
48
+ };
49
+ });
50
+ const [positionStyles, setPositionStyles] = useState({
51
+ display: 'none'
52
+ });
53
+
54
+ // Same positioning pattern as quick insert / drag handle: anchor(start/end) or offset-based left/top
55
+ const calculatePosition = useCallback(() => {
56
+ var _innerContainer, _dom$offsetLeft;
57
+ const safeAnchorName = refreshAnchorName({
58
+ getPos,
59
+ view,
60
+ anchorName: rootAnchorName !== null && rootAnchorName !== void 0 ? rootAnchorName : anchorName
61
+ });
62
+ const dom = view.dom.querySelector(`[${getAnchorAttrName()}="${safeAnchorName}"]`);
63
+ if (!dom) {
64
+ return {
65
+ display: 'none'
66
+ };
67
+ }
68
+ const hasResizer = rootNodeType === 'table' || rootNodeType === 'mediaSingle';
69
+ const isExtension = rootNodeType === 'extension' || rootNodeType === 'bodiedExtension';
70
+ const isBlockCard = rootNodeType === 'blockCard';
71
+ const isEmbedCard = rootNodeType === 'embedCard';
72
+ const isMacroInteractionUpdates = macroInteractionUpdates && isExtension;
73
+ let innerContainer = null;
74
+ if (dom) {
75
+ if (isEmbedCard) {
76
+ innerContainer = dom.querySelector('.rich-media-item');
77
+ } else if (hasResizer) {
78
+ innerContainer = dom.querySelector('.resizer-item');
79
+ } else if (isExtension) {
80
+ innerContainer = dom.querySelector('.extension-container[data-layout]');
81
+ } else if (isBlockCard) {
82
+ innerContainer = dom.querySelector('.datasourceView-content-inner-wrap');
83
+ }
84
+ }
85
+ const isEdgeCase = (hasResizer || isExtension || isEmbedCard || isBlockCard) && innerContainer;
86
+
87
+ // Check anchor first (no reflow). Only call expensive getRightPositionForRootElement when fallback needed.
88
+ const supportsAnchorRight = CSS.supports('left', `anchor(${safeAnchorName} end)`) && CSS.supports('top', `anchor(${safeAnchorName} start)`);
89
+ if (supportsAnchorRight && !isEdgeCase) {
90
+ return {
91
+ left: `calc(anchor(${safeAnchorName} end) - ${REMIX_BUTTON_DIMENSIONS.width}px - ${rootElementGap(rootNodeType)}px + ${REMIX_BUTTON_RIGHT_OFFSET}px)`,
92
+ top: `calc(anchor(${safeAnchorName} start) + ${topPositionAdjustment(rootNodeType)}px)`,
93
+ height: `${REMIX_BUTTON_DIMENSIONS.height}px`,
94
+ bottom: 'unset'
95
+ };
96
+ }
97
+ // Fallback: offset-based (triggers reflow). When isEdgeCase add dom.offsetLeft (same as left controls).
98
+ const rightEdgeLeft = getRightPositionForRootElement(dom, rootNodeType, REMIX_BUTTON_DIMENSIONS, (_innerContainer = innerContainer) !== null && _innerContainer !== void 0 ? _innerContainer : undefined, isMacroInteractionUpdates);
99
+ return {
100
+ left: isEdgeCase ? `calc(${(_dom$offsetLeft = dom === null || dom === void 0 ? void 0 : dom.offsetLeft) !== null && _dom$offsetLeft !== void 0 ? _dom$offsetLeft : 0}px + (${rightEdgeLeft}) + ${REMIX_BUTTON_RIGHT_OFFSET}px)` : `calc(${rightEdgeLeft} + ${REMIX_BUTTON_RIGHT_OFFSET}px)`,
101
+ top: getTopPosition(dom, rootNodeType),
102
+ height: `${REMIX_BUTTON_DIMENSIONS.height}px`,
103
+ bottom: 'unset'
104
+ };
105
+ }, [view, getPos, anchorName, rootAnchorName, rootNodeType, macroInteractionUpdates]);
106
+
107
+ // Recompute button position on mount and when extension/embedCard layout changes (e.g. expand/collapse).
108
+ // For extension/embedCard we listen to transitionend so position updates after CSS transitions finish.
109
+ useEffect(() => {
110
+ let cleanUpTransitionListener;
111
+ if (rootNodeType === 'extension' || rootNodeType === 'embedCard') {
112
+ const anchorDom = view.dom.querySelector(`[${getAnchorAttrName()}="${rootAnchorName !== null && rootAnchorName !== void 0 ? rootAnchorName : anchorName}"]`);
113
+ if (anchorDom) {
114
+ cleanUpTransitionListener = bind(anchorDom, {
115
+ type: 'transitionend',
116
+ listener: () => setPositionStyles(calculatePosition())
117
+ });
118
+ }
119
+ }
120
+ const id = requestAnimationFrame(() => setPositionStyles(calculatePosition()));
121
+ return () => {
122
+ var _cleanUpTransitionLis;
123
+ cancelAnimationFrame(id);
124
+ (_cleanUpTransitionLis = cleanUpTransitionListener) === null || _cleanUpTransitionLis === void 0 ? void 0 : _cleanUpTransitionLis();
125
+ };
126
+ }, [calculatePosition, view.dom, rootAnchorName, rootNodeType, anchorName]);
127
+ return jsx(VisibilityContainer, {
128
+ api: api
129
+ }, jsx("div", {
130
+ css: containerBaseStyles
131
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Dynamic positioning (left, top, height) calculated at runtime
132
+ ,
133
+ style: positionStyles,
134
+ "data-testid": "block-ctrl-remix-button"
135
+ }, jsx(IconButton, {
136
+ spacing: "compact",
137
+ appearance: "subtle",
138
+ label: "Remix",
139
+ icon: () => jsx(RandomizeIcon, {
140
+ label: ""
141
+ }),
142
+ onMouseDown: e => e.preventDefault()
143
+ })));
144
+ };
@@ -0,0 +1,59 @@
1
+ import { createElement } from 'react';
2
+ // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
3
+ import uuid from 'uuid';
4
+ import { getDocument } from '@atlaskit/browser-apis';
5
+ import { Decoration } from '@atlaskit/editor-prosemirror/view';
6
+ import { fg } from '@atlaskit/platform-feature-flags';
7
+ import { RemixButton } from '../ui/remix-button';
8
+ var TYPE_REMIX_BUTTON = 'REMIX_BUTTON';
9
+ export var findRemixButtonDecoration = function findRemixButtonDecoration(decorations, from, to) {
10
+ return decorations.find(from, to, function (spec) {
11
+ return spec.type === TYPE_REMIX_BUTTON;
12
+ });
13
+ };
14
+ /** Right-edge Remix button: same gutter as left controls (side: -4) but positioned at block right edge. */
15
+ export var remixButtonDecoration = function remixButtonDecoration(_ref) {
16
+ var api = _ref.api,
17
+ formatMessage = _ref.formatMessage,
18
+ rootPos = _ref.rootPos,
19
+ anchorName = _ref.anchorName,
20
+ nodeType = _ref.nodeType,
21
+ nodeViewPortalProviderAPI = _ref.nodeViewPortalProviderAPI,
22
+ rootAnchorName = _ref.rootAnchorName,
23
+ rootNodeType = _ref.rootNodeType;
24
+ // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
25
+ var key = uuid();
26
+ var widgetSpec = {
27
+ side: -4,
28
+ type: TYPE_REMIX_BUTTON,
29
+ destroy: function destroy(_) {
30
+ if (fg('platform_editor_fix_widget_destroy')) {
31
+ nodeViewPortalProviderAPI.remove(key);
32
+ }
33
+ }
34
+ };
35
+ return Decoration.widget(rootPos, function (view, getPos) {
36
+ var doc = getDocument();
37
+ if (!doc) {
38
+ throw new Error('Document not available');
39
+ }
40
+ var element = doc.createElement('span');
41
+ element.style.display = 'block';
42
+ element.contentEditable = 'false';
43
+ element.setAttribute('data-blocks-remix-button-container', 'true');
44
+ element.setAttribute('data-testid', 'block-ctrl-remix-button-container');
45
+ nodeViewPortalProviderAPI.render(function () {
46
+ return /*#__PURE__*/createElement(RemixButton, {
47
+ api: api,
48
+ getPos: getPos,
49
+ formatMessage: formatMessage,
50
+ view: view,
51
+ nodeType: nodeType,
52
+ anchorName: anchorName,
53
+ rootAnchorName: rootAnchorName,
54
+ rootNodeType: rootNodeType !== null && rootNodeType !== void 0 ? rootNodeType : nodeType
55
+ });
56
+ }, element, key, undefined, true);
57
+ return element;
58
+ }, widgetSpec);
59
+ };
@@ -26,6 +26,7 @@ import { dragHandleDecoration, emptyParagraphNodeDecorations, findHandleDec } fr
26
26
  import { dropTargetDecorations, findDropTargetDecs } from './decorations-drop-target';
27
27
  import { getActiveDropTargetDecorations } from './decorations-drop-target-active';
28
28
  import { findQuickInsertInsertButtonDecoration, quickInsertButtonDecoration } from './decorations-quick-insert-button';
29
+ import { findRemixButtonDecoration, remixButtonDecoration } from './decorations-remix-button';
29
30
  import { handleMouseDown } from './handle-mouse-down';
30
31
  import { handleMouseOver } from './handle-mouse-over';
31
32
  import { boundKeydownHandler } from './keymap';
@@ -432,12 +433,17 @@ var _apply = function apply(api, formatMessage, tr, currentState, newState, flag
432
433
  var _activeNode7, _activeNode8;
433
434
  var oldQuickInsertButton = findQuickInsertInsertButtonDecoration(decorations, (_activeNode7 = activeNode) === null || _activeNode7 === void 0 ? void 0 : _activeNode7.rootPos, (_activeNode8 = activeNode) === null || _activeNode8 === void 0 ? void 0 : _activeNode8.rootPos);
434
435
  decorations = decorations.remove(oldQuickInsertButton);
436
+ if (expValEquals('confluence_remix_icon_right_side', 'isEnabled', true)) {
437
+ var _activeNode9, _activeNode0;
438
+ var oldRemixButton = findRemixButtonDecoration(decorations, (_activeNode9 = activeNode) === null || _activeNode9 === void 0 ? void 0 : _activeNode9.rootPos, (_activeNode0 = activeNode) === null || _activeNode0 === void 0 ? void 0 : _activeNode0.rootPos);
439
+ decorations = decorations.remove(oldRemixButton);
440
+ }
435
441
  }
436
442
  } else if (api) {
437
443
  var _latestActiveNode5;
438
444
  if (shouldRecreateHandle) {
439
- var _activeNode9, _activeNode0, _latestActiveNode, _latestActiveNode2, _latestActiveNode3, _latestActiveNode4;
440
- var _oldHandle = findHandleDec(decorations, (_activeNode9 = activeNode) === null || _activeNode9 === void 0 ? void 0 : _activeNode9.pos, (_activeNode0 = activeNode) === null || _activeNode0 === void 0 ? void 0 : _activeNode0.pos);
445
+ var _activeNode1, _activeNode10, _latestActiveNode, _latestActiveNode2, _latestActiveNode3, _latestActiveNode4;
446
+ var _oldHandle = findHandleDec(decorations, (_activeNode1 = activeNode) === null || _activeNode1 === void 0 ? void 0 : _activeNode1.pos, (_activeNode10 = activeNode) === null || _activeNode10 === void 0 ? void 0 : _activeNode10.pos);
441
447
  decorations = decorations.remove(_oldHandle);
442
448
  var handleDec = dragHandleDecoration({
443
449
  api: api,
@@ -455,8 +461,8 @@ var _apply = function apply(api, formatMessage, tr, currentState, newState, flag
455
461
  if (shouldRecreateQuickInsertButton && ((_latestActiveNode5 = latestActiveNode) === null || _latestActiveNode5 === void 0 ? void 0 : _latestActiveNode5.rootPos) !== undefined &&
456
462
  // platform_editor_controls note: enables quick insert
457
463
  flags.toolbarFlagsEnabled) {
458
- var _activeNode1, _activeNode10, _latestActiveNode6, _latestActiveNode7, _latestActiveNode8, _latestActiveNode9, _latestActiveNode0;
459
- var _oldQuickInsertButton = findQuickInsertInsertButtonDecoration(decorations, (_activeNode1 = activeNode) === null || _activeNode1 === void 0 ? void 0 : _activeNode1.rootPos, (_activeNode10 = activeNode) === null || _activeNode10 === void 0 ? void 0 : _activeNode10.rootPos);
464
+ var _activeNode11, _activeNode12, _latestActiveNode6, _latestActiveNode7, _latestActiveNode8, _latestActiveNode9, _latestActiveNode0;
465
+ var _oldQuickInsertButton = findQuickInsertInsertButtonDecoration(decorations, (_activeNode11 = activeNode) === null || _activeNode11 === void 0 ? void 0 : _activeNode11.rootPos, (_activeNode12 = activeNode) === null || _activeNode12 === void 0 ? void 0 : _activeNode12.rootPos);
460
466
  decorations = decorations.remove(_oldQuickInsertButton);
461
467
  var quickInsertButton = quickInsertButtonDecoration({
462
468
  api: api,
@@ -471,6 +477,28 @@ var _apply = function apply(api, formatMessage, tr, currentState, newState, flag
471
477
  editorState: newState
472
478
  });
473
479
  decorations = decorations.add(newState.doc, [quickInsertButton]);
480
+ if (expValEquals('confluence_remix_icon_right_side', 'isEnabled', true)) {
481
+ var _activeNode13, _activeNode14, _latestActiveNode1, _latestActiveNode10, _latestActiveNode11, _latestActiveNode12, _latestActiveNode13;
482
+ var _oldRemixButton = findRemixButtonDecoration(decorations, (_activeNode13 = activeNode) === null || _activeNode13 === void 0 ? void 0 : _activeNode13.rootPos, (_activeNode14 = activeNode) === null || _activeNode14 === void 0 ? void 0 : _activeNode14.rootPos);
483
+ decorations = decorations.remove(_oldRemixButton);
484
+ var remixButton = remixButtonDecoration({
485
+ api: api,
486
+ formatMessage: formatMessage,
487
+ anchorName: (_latestActiveNode1 = latestActiveNode) === null || _latestActiveNode1 === void 0 ? void 0 : _latestActiveNode1.anchorName,
488
+ nodeType: (_latestActiveNode10 = latestActiveNode) === null || _latestActiveNode10 === void 0 ? void 0 : _latestActiveNode10.nodeType,
489
+ nodeViewPortalProviderAPI: nodeViewPortalProviderAPI,
490
+ rootPos: (_latestActiveNode11 = latestActiveNode) === null || _latestActiveNode11 === void 0 ? void 0 : _latestActiveNode11.rootPos,
491
+ rootAnchorName: (_latestActiveNode12 = latestActiveNode) === null || _latestActiveNode12 === void 0 ? void 0 : _latestActiveNode12.rootAnchorName,
492
+ rootNodeType: (_latestActiveNode13 = latestActiveNode) === null || _latestActiveNode13 === void 0 ? void 0 : _latestActiveNode13.rootNodeType,
493
+ editorState: newState
494
+ });
495
+ decorations = decorations.add(newState.doc, [remixButton]);
496
+ } else {
497
+ var _activeNode15, _activeNode16;
498
+ // Remove remix decoration when experiment is off so it disappears when flag is toggled
499
+ var _oldRemixButton2 = findRemixButtonDecoration(decorations, (_activeNode15 = activeNode) === null || _activeNode15 === void 0 ? void 0 : _activeNode15.rootPos, (_activeNode16 = activeNode) === null || _activeNode16 === void 0 ? void 0 : _activeNode16.rootPos);
500
+ decorations = decorations.remove(_oldRemixButton2);
501
+ }
474
502
  }
475
503
  }
476
504
 
@@ -518,12 +546,12 @@ var _apply = function apply(api, formatMessage, tr, currentState, newState, flag
518
546
  var newActiveNode;
519
547
  // platform_editor_controls note: enables quick insert
520
548
  if (flags.toolbarFlagsEnabled) {
521
- var _latestActiveNode1, _latestActiveNode10;
549
+ var _latestActiveNode14, _latestActiveNode15;
522
550
  // remove isEmptyDoc check and let decorations render and determine their own visibility
523
- newActiveNode = !(meta !== null && meta !== void 0 && meta.activeNode) && findHandleDec(decorations, (_latestActiveNode1 = latestActiveNode) === null || _latestActiveNode1 === void 0 ? void 0 : _latestActiveNode1.pos, (_latestActiveNode10 = latestActiveNode) === null || _latestActiveNode10 === void 0 ? void 0 : _latestActiveNode10.pos).length === 0 ? null : latestActiveNode;
551
+ newActiveNode = !(meta !== null && meta !== void 0 && meta.activeNode) && findHandleDec(decorations, (_latestActiveNode14 = latestActiveNode) === null || _latestActiveNode14 === void 0 ? void 0 : _latestActiveNode14.pos, (_latestActiveNode15 = latestActiveNode) === null || _latestActiveNode15 === void 0 ? void 0 : _latestActiveNode15.pos).length === 0 ? null : latestActiveNode;
524
552
  } else {
525
- var _latestActiveNode11, _latestActiveNode12;
526
- newActiveNode = isEmptyDoc || !(meta !== null && meta !== void 0 && meta.activeNode) && findHandleDec(decorations, (_latestActiveNode11 = latestActiveNode) === null || _latestActiveNode11 === void 0 ? void 0 : _latestActiveNode11.pos, (_latestActiveNode12 = latestActiveNode) === null || _latestActiveNode12 === void 0 ? void 0 : _latestActiveNode12.pos).length === 0 ? null : latestActiveNode;
553
+ var _latestActiveNode16, _latestActiveNode17;
554
+ newActiveNode = isEmptyDoc || !(meta !== null && meta !== void 0 && meta.activeNode) && findHandleDec(decorations, (_latestActiveNode16 = latestActiveNode) === null || _latestActiveNode16 === void 0 ? void 0 : _latestActiveNode16.pos, (_latestActiveNode17 = latestActiveNode) === null || _latestActiveNode17 === void 0 ? void 0 : _latestActiveNode17.pos).length === 0 ? null : latestActiveNode;
527
555
  }
528
556
  var isMenuOpenNew = isMenuOpen;
529
557
  if (expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true)) {
@@ -17,4 +17,24 @@ export var getLeftPositionForRootElement = function getLeftPositionForRootElemen
17
17
  var relativeSpan = macroInteractionUpdates ? dom.querySelector('span.relative') : null;
18
18
  var leftAdjustment = relativeSpan ? relativeSpan.offsetLeft : 0;
19
19
  return getComputedStyle(innerContainer).transform === 'none' ? "".concat(innerContainer.offsetLeft + leftAdjustment - rootElementGap(nodeType) - widgetDimensions.width, "px") : "".concat(innerContainer.offsetLeft + leftAdjustment - innerContainer.offsetWidth / 2 - rootElementGap(nodeType) - widgetDimensions.width, "px");
20
+ };
21
+
22
+ /**
23
+ * Left position (in px) for a widget on the right edge of the block.
24
+ * Mirrors getLeftPositionForRootElement: when innerContainer is set (table, mediaSingle,
25
+ * extension, blockCard, embedCard) use it for the right edge so the button aligns to the
26
+ * content box and does not overlap; otherwise use the block (dom).
27
+ */
28
+ export var getRightPositionForRootElement = function getRightPositionForRootElement(dom, nodeType, widgetDimensions, innerContainer, macroInteractionUpdates) {
29
+ if (!dom) {
30
+ return 'auto';
31
+ }
32
+ var relativeSpan = macroInteractionUpdates ? dom.querySelector('span.relative') : null;
33
+ var leftAdjustment = relativeSpan ? relativeSpan.offsetLeft : 0;
34
+ var gap = rootElementGap(nodeType);
35
+ var width = widgetDimensions.width;
36
+ if (!innerContainer) {
37
+ return "".concat(dom.offsetLeft + leftAdjustment + dom.offsetWidth - gap - width, "px");
38
+ }
39
+ return getComputedStyle(innerContainer).transform === 'none' ? "".concat(innerContainer.offsetLeft + leftAdjustment + innerContainer.offsetWidth - gap - width, "px") : "".concat(innerContainer.offsetLeft + leftAdjustment + innerContainer.offsetWidth / 2 - gap - width, "px");
20
40
  };
@@ -37,6 +37,14 @@ export var QUICK_INSERT_DIMENSIONS = {
37
37
  height: QUICK_INSERT_HEIGHT
38
38
  };
39
39
  export var QUICK_INSERT_LEFT_OFFSET = 16;
40
+ export var REMIX_BUTTON_HEIGHT = 24;
41
+ export var REMIX_BUTTON_WIDTH = 24;
42
+ export var REMIX_BUTTON_DIMENSIONS = {
43
+ width: REMIX_BUTTON_WIDTH,
44
+ height: REMIX_BUTTON_HEIGHT
45
+ };
46
+ /** Extra offset to the right for the right-side Remix button (px) */
47
+ export var REMIX_BUTTON_RIGHT_OFFSET = 55;
40
48
  var nodeTypeExcludeList = ['embedCard', 'mediaSingle', 'table'];
41
49
  var breakoutResizableNodes = ['expand', 'layoutSection', 'codeBlock'];
42
50
  export var dragHandleGap = function dragHandleGap(nodeType, parentNodeType) {
@@ -0,0 +1,154 @@
1
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
+ /**
3
+ * Remix block control: positioned at the right edge of the block.
4
+ * Uses anchor(anchorName end) or getRightPositionForRootElement (same coordinate system
5
+ * as left controls). The widget span has no position:relative so position:absolute
6
+ * uses the same containing block as the node.
7
+ */
8
+ /* @jsxRuntime classic */
9
+ /**
10
+ * @jsxRuntime classic
11
+ * @jsx jsx
12
+ */
13
+
14
+ import React, { useCallback, useEffect, useState } from 'react';
15
+
16
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
17
+ import { css, jsx } from '@emotion/react';
18
+ import { bind } from 'bind-event-listener';
19
+ import { IconButton } from '@atlaskit/button/new';
20
+ import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
21
+ import RandomizeIcon from '@atlaskit/icon-lab/core/randomize';
22
+ import { getTopPosition } from '../pm-plugins/utils/drag-handle-positions';
23
+ import { getRightPositionForRootElement } from '../pm-plugins/utils/widget-positions';
24
+ import { REMIX_BUTTON_DIMENSIONS, REMIX_BUTTON_RIGHT_OFFSET, rootElementGap, topPositionAdjustment } from './consts';
25
+ import { refreshAnchorName } from './utils/anchor-name';
26
+ import { getAnchorAttrName } from './utils/dom-attr-name';
27
+ import { VisibilityContainer } from './visibility-container';
28
+ var containerBaseStyles = css({
29
+ position: 'absolute',
30
+ zIndex: 100,
31
+ display: 'flex',
32
+ alignItems: 'center',
33
+ justifyContent: 'center'
34
+ });
35
+ export var RemixButton = function RemixButton(_ref) {
36
+ var view = _ref.view,
37
+ api = _ref.api,
38
+ getPos = _ref.getPos,
39
+ anchorName = _ref.anchorName,
40
+ rootAnchorName = _ref.rootAnchorName,
41
+ rootNodeType = _ref.rootNodeType;
42
+ var _useSharedPluginState = useSharedPluginStateWithSelector(api, ['featureFlags'], function (states) {
43
+ var _states$featureFlagsS;
44
+ return {
45
+ macroInteractionUpdates: (_states$featureFlagsS = states.featureFlagsState) === null || _states$featureFlagsS === void 0 ? void 0 : _states$featureFlagsS.macroInteractionUpdates
46
+ };
47
+ }),
48
+ macroInteractionUpdates = _useSharedPluginState.macroInteractionUpdates;
49
+ var _useState = useState({
50
+ display: 'none'
51
+ }),
52
+ _useState2 = _slicedToArray(_useState, 2),
53
+ positionStyles = _useState2[0],
54
+ setPositionStyles = _useState2[1];
55
+
56
+ // Same positioning pattern as quick insert / drag handle: anchor(start/end) or offset-based left/top
57
+ var calculatePosition = useCallback(function () {
58
+ var _dom$offsetLeft;
59
+ var safeAnchorName = refreshAnchorName({
60
+ getPos: getPos,
61
+ view: view,
62
+ anchorName: rootAnchorName !== null && rootAnchorName !== void 0 ? rootAnchorName : anchorName
63
+ });
64
+ var dom = view.dom.querySelector("[".concat(getAnchorAttrName(), "=\"").concat(safeAnchorName, "\"]"));
65
+ if (!dom) {
66
+ return {
67
+ display: 'none'
68
+ };
69
+ }
70
+ var hasResizer = rootNodeType === 'table' || rootNodeType === 'mediaSingle';
71
+ var isExtension = rootNodeType === 'extension' || rootNodeType === 'bodiedExtension';
72
+ var isBlockCard = rootNodeType === 'blockCard';
73
+ var isEmbedCard = rootNodeType === 'embedCard';
74
+ var isMacroInteractionUpdates = macroInteractionUpdates && isExtension;
75
+ var innerContainer = null;
76
+ if (dom) {
77
+ if (isEmbedCard) {
78
+ innerContainer = dom.querySelector('.rich-media-item');
79
+ } else if (hasResizer) {
80
+ innerContainer = dom.querySelector('.resizer-item');
81
+ } else if (isExtension) {
82
+ innerContainer = dom.querySelector('.extension-container[data-layout]');
83
+ } else if (isBlockCard) {
84
+ innerContainer = dom.querySelector('.datasourceView-content-inner-wrap');
85
+ }
86
+ }
87
+ var isEdgeCase = (hasResizer || isExtension || isEmbedCard || isBlockCard) && innerContainer;
88
+
89
+ // Check anchor first (no reflow). Only call expensive getRightPositionForRootElement when fallback needed.
90
+ var supportsAnchorRight = CSS.supports('left', "anchor(".concat(safeAnchorName, " end)")) && CSS.supports('top', "anchor(".concat(safeAnchorName, " start)"));
91
+ if (supportsAnchorRight && !isEdgeCase) {
92
+ return {
93
+ left: "calc(anchor(".concat(safeAnchorName, " end) - ").concat(REMIX_BUTTON_DIMENSIONS.width, "px - ").concat(rootElementGap(rootNodeType), "px + ").concat(REMIX_BUTTON_RIGHT_OFFSET, "px)"),
94
+ top: "calc(anchor(".concat(safeAnchorName, " start) + ").concat(topPositionAdjustment(rootNodeType), "px)"),
95
+ height: "".concat(REMIX_BUTTON_DIMENSIONS.height, "px"),
96
+ bottom: 'unset'
97
+ };
98
+ }
99
+ // Fallback: offset-based (triggers reflow). When isEdgeCase add dom.offsetLeft (same as left controls).
100
+ var rightEdgeLeft = getRightPositionForRootElement(dom, rootNodeType, REMIX_BUTTON_DIMENSIONS, innerContainer !== null && innerContainer !== void 0 ? innerContainer : undefined, isMacroInteractionUpdates);
101
+ return {
102
+ left: isEdgeCase ? "calc(".concat((_dom$offsetLeft = dom === null || dom === void 0 ? void 0 : dom.offsetLeft) !== null && _dom$offsetLeft !== void 0 ? _dom$offsetLeft : 0, "px + (").concat(rightEdgeLeft, ") + ").concat(REMIX_BUTTON_RIGHT_OFFSET, "px)") : "calc(".concat(rightEdgeLeft, " + ").concat(REMIX_BUTTON_RIGHT_OFFSET, "px)"),
103
+ top: getTopPosition(dom, rootNodeType),
104
+ height: "".concat(REMIX_BUTTON_DIMENSIONS.height, "px"),
105
+ bottom: 'unset'
106
+ };
107
+ }, [view, getPos, anchorName, rootAnchorName, rootNodeType, macroInteractionUpdates]);
108
+
109
+ // Recompute button position on mount and when extension/embedCard layout changes (e.g. expand/collapse).
110
+ // For extension/embedCard we listen to transitionend so position updates after CSS transitions finish.
111
+ useEffect(function () {
112
+ var cleanUpTransitionListener;
113
+ if (rootNodeType === 'extension' || rootNodeType === 'embedCard') {
114
+ var anchorDom = view.dom.querySelector("[".concat(getAnchorAttrName(), "=\"").concat(rootAnchorName !== null && rootAnchorName !== void 0 ? rootAnchorName : anchorName, "\"]"));
115
+ if (anchorDom) {
116
+ cleanUpTransitionListener = bind(anchorDom, {
117
+ type: 'transitionend',
118
+ listener: function listener() {
119
+ return setPositionStyles(calculatePosition());
120
+ }
121
+ });
122
+ }
123
+ }
124
+ var id = requestAnimationFrame(function () {
125
+ return setPositionStyles(calculatePosition());
126
+ });
127
+ return function () {
128
+ var _cleanUpTransitionLis;
129
+ cancelAnimationFrame(id);
130
+ (_cleanUpTransitionLis = cleanUpTransitionListener) === null || _cleanUpTransitionLis === void 0 || _cleanUpTransitionLis();
131
+ };
132
+ }, [calculatePosition, view.dom, rootAnchorName, rootNodeType, anchorName]);
133
+ return jsx(VisibilityContainer, {
134
+ api: api
135
+ }, jsx("div", {
136
+ css: containerBaseStyles
137
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Dynamic positioning (left, top, height) calculated at runtime
138
+ ,
139
+ style: positionStyles,
140
+ "data-testid": "block-ctrl-remix-button"
141
+ }, jsx(IconButton, {
142
+ spacing: "compact",
143
+ appearance: "subtle",
144
+ label: "Remix",
145
+ icon: function icon() {
146
+ return jsx(RandomizeIcon, {
147
+ label: ""
148
+ });
149
+ },
150
+ onMouseDown: function onMouseDown(e) {
151
+ return e.preventDefault();
152
+ }
153
+ })));
154
+ };
@@ -0,0 +1,21 @@
1
+ import { type IntlShape } from 'react-intl-next';
2
+ import type { PortalProviderAPI } from '@atlaskit/editor-common/portal';
3
+ import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
4
+ import { type EditorState } from '@atlaskit/editor-prosemirror/state';
5
+ import { Decoration, type DecorationSet } from '@atlaskit/editor-prosemirror/view';
6
+ import type { BlockControlsPlugin } from '../blockControlsPluginType';
7
+ export declare const findRemixButtonDecoration: (decorations: DecorationSet, from?: number, to?: number) => Decoration[];
8
+ type RemixButtonDecorationParams = {
9
+ anchorName: string;
10
+ api: ExtractInjectionAPI<BlockControlsPlugin>;
11
+ editorState: EditorState;
12
+ formatMessage: IntlShape['formatMessage'];
13
+ nodeType: string;
14
+ nodeViewPortalProviderAPI: PortalProviderAPI;
15
+ rootAnchorName?: string;
16
+ rootNodeType?: string;
17
+ rootPos: number;
18
+ };
19
+ /** Right-edge Remix button: same gutter as left controls (side: -4) but positioned at block right edge. */
20
+ export declare const remixButtonDecoration: ({ api, formatMessage, rootPos, anchorName, nodeType, nodeViewPortalProviderAPI, rootAnchorName, rootNodeType, }: RemixButtonDecorationParams) => Decoration;
21
+ export {};
@@ -2,3 +2,13 @@ export declare const getLeftPositionForRootElement: (dom: HTMLElement | null, no
2
2
  height: number;
3
3
  width: number;
4
4
  }, innerContainer?: HTMLElement | null, macroInteractionUpdates?: boolean) => string;
5
+ /**
6
+ * Left position (in px) for a widget on the right edge of the block.
7
+ * Mirrors getLeftPositionForRootElement: when innerContainer is set (table, mediaSingle,
8
+ * extension, blockCard, embedCard) use it for the right edge so the button aligns to the
9
+ * content box and does not overlap; otherwise use the block (dom).
10
+ */
11
+ export declare const getRightPositionForRootElement: (dom: HTMLElement | null, nodeType: string, widgetDimensions: {
12
+ height: number;
13
+ width: number;
14
+ }, innerContainer?: HTMLElement | null, macroInteractionUpdates?: boolean) => string;
@@ -30,6 +30,14 @@ export declare const QUICK_INSERT_DIMENSIONS: {
30
30
  height: number;
31
31
  };
32
32
  export declare const QUICK_INSERT_LEFT_OFFSET = 16;
33
+ export declare const REMIX_BUTTON_HEIGHT = 24;
34
+ export declare const REMIX_BUTTON_WIDTH = 24;
35
+ export declare const REMIX_BUTTON_DIMENSIONS: {
36
+ width: number;
37
+ height: number;
38
+ };
39
+ /** Extra offset to the right for the right-side Remix button (px) */
40
+ export declare const REMIX_BUTTON_RIGHT_OFFSET = 55;
33
41
  export declare const dragHandleGap: (nodeType: string, parentNodeType?: string) => number;
34
42
  export declare const rootElementGap: (nodeType: string) => number;
35
43
  export declare const getNestedNodeLeftPaddingMargin: (nodeType?: string) => "8px" | "16px" | "20px" | "24px" | "28px" | "40px";
@@ -0,0 +1,17 @@
1
+ import { jsx } from '@emotion/react';
2
+ import type { IntlShape } from 'react-intl-next';
3
+ import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
4
+ import type { EditorView } from '@atlaskit/editor-prosemirror/view';
5
+ import type { BlockControlsPlugin } from '../blockControlsPluginType';
6
+ type RemixButtonProps = {
7
+ anchorName: string;
8
+ api: ExtractInjectionAPI<BlockControlsPlugin>;
9
+ formatMessage: IntlShape['formatMessage'];
10
+ getPos: () => number | undefined;
11
+ nodeType: string;
12
+ rootAnchorName?: string;
13
+ rootNodeType: string;
14
+ view: EditorView;
15
+ };
16
+ export declare const RemixButton: ({ view, api, getPos, anchorName, rootAnchorName, rootNodeType, }: RemixButtonProps) => jsx.JSX.Element;
17
+ export {};
@@ -8,5 +8,5 @@ type RefreshAnchorNameParams = {
8
8
  * Checks for plugin state for latest anchorName based on the position, returns
9
9
  * provided anchorName if available
10
10
  */
11
- export declare const refreshAnchorName: ({ getPos, view, anchorName }: RefreshAnchorNameParams) => string;
11
+ export declare const refreshAnchorName: ({ getPos, view, anchorName, }: RefreshAnchorNameParams) => string;
12
12
  export {};
@@ -0,0 +1,21 @@
1
+ import { type IntlShape } from 'react-intl-next';
2
+ import type { PortalProviderAPI } from '@atlaskit/editor-common/portal';
3
+ import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
4
+ import { type EditorState } from '@atlaskit/editor-prosemirror/state';
5
+ import { Decoration, type DecorationSet } from '@atlaskit/editor-prosemirror/view';
6
+ import type { BlockControlsPlugin } from '../blockControlsPluginType';
7
+ export declare const findRemixButtonDecoration: (decorations: DecorationSet, from?: number, to?: number) => Decoration[];
8
+ type RemixButtonDecorationParams = {
9
+ anchorName: string;
10
+ api: ExtractInjectionAPI<BlockControlsPlugin>;
11
+ editorState: EditorState;
12
+ formatMessage: IntlShape['formatMessage'];
13
+ nodeType: string;
14
+ nodeViewPortalProviderAPI: PortalProviderAPI;
15
+ rootAnchorName?: string;
16
+ rootNodeType?: string;
17
+ rootPos: number;
18
+ };
19
+ /** Right-edge Remix button: same gutter as left controls (side: -4) but positioned at block right edge. */
20
+ export declare const remixButtonDecoration: ({ api, formatMessage, rootPos, anchorName, nodeType, nodeViewPortalProviderAPI, rootAnchorName, rootNodeType, }: RemixButtonDecorationParams) => Decoration;
21
+ export {};
@@ -2,3 +2,13 @@ export declare const getLeftPositionForRootElement: (dom: HTMLElement | null, no
2
2
  height: number;
3
3
  width: number;
4
4
  }, innerContainer?: HTMLElement | null, macroInteractionUpdates?: boolean) => string;
5
+ /**
6
+ * Left position (in px) for a widget on the right edge of the block.
7
+ * Mirrors getLeftPositionForRootElement: when innerContainer is set (table, mediaSingle,
8
+ * extension, blockCard, embedCard) use it for the right edge so the button aligns to the
9
+ * content box and does not overlap; otherwise use the block (dom).
10
+ */
11
+ export declare const getRightPositionForRootElement: (dom: HTMLElement | null, nodeType: string, widgetDimensions: {
12
+ height: number;
13
+ width: number;
14
+ }, innerContainer?: HTMLElement | null, macroInteractionUpdates?: boolean) => string;
@@ -30,6 +30,14 @@ export declare const QUICK_INSERT_DIMENSIONS: {
30
30
  height: number;
31
31
  };
32
32
  export declare const QUICK_INSERT_LEFT_OFFSET = 16;
33
+ export declare const REMIX_BUTTON_HEIGHT = 24;
34
+ export declare const REMIX_BUTTON_WIDTH = 24;
35
+ export declare const REMIX_BUTTON_DIMENSIONS: {
36
+ width: number;
37
+ height: number;
38
+ };
39
+ /** Extra offset to the right for the right-side Remix button (px) */
40
+ export declare const REMIX_BUTTON_RIGHT_OFFSET = 55;
33
41
  export declare const dragHandleGap: (nodeType: string, parentNodeType?: string) => number;
34
42
  export declare const rootElementGap: (nodeType: string) => number;
35
43
  export declare const getNestedNodeLeftPaddingMargin: (nodeType?: string) => "8px" | "16px" | "20px" | "24px" | "28px" | "40px";
@@ -0,0 +1,17 @@
1
+ import { jsx } from '@emotion/react';
2
+ import type { IntlShape } from 'react-intl-next';
3
+ import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
4
+ import type { EditorView } from '@atlaskit/editor-prosemirror/view';
5
+ import type { BlockControlsPlugin } from '../blockControlsPluginType';
6
+ type RemixButtonProps = {
7
+ anchorName: string;
8
+ api: ExtractInjectionAPI<BlockControlsPlugin>;
9
+ formatMessage: IntlShape['formatMessage'];
10
+ getPos: () => number | undefined;
11
+ nodeType: string;
12
+ rootAnchorName?: string;
13
+ rootNodeType: string;
14
+ view: EditorView;
15
+ };
16
+ export declare const RemixButton: ({ view, api, getPos, anchorName, rootAnchorName, rootNodeType, }: RemixButtonProps) => jsx.JSX.Element;
17
+ export {};
@@ -8,5 +8,5 @@ type RefreshAnchorNameParams = {
8
8
  * Checks for plugin state for latest anchorName based on the position, returns
9
9
  * provided anchorName if available
10
10
  */
11
- export declare const refreshAnchorName: ({ getPos, view, anchorName }: RefreshAnchorNameParams) => string;
11
+ export declare const refreshAnchorName: ({ getPos, view, anchorName, }: RefreshAnchorNameParams) => string;
12
12
  export {};