@atlaskit/editor-plugin-block-controls 8.6.3 → 8.7.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/block-decoration-utils/package.json +17 -0
  3. package/dist/cjs/blockControlsPlugin.js +15 -1
  4. package/dist/cjs/pm-plugins/main.js +56 -29
  5. package/dist/cjs/ui/block-decoration-utils.js +53 -0
  6. package/dist/cjs/ui/consts.js +1 -9
  7. package/dist/es2019/blockControlsPlugin.js +273 -259
  8. package/dist/es2019/pm-plugins/main.js +25 -29
  9. package/dist/es2019/ui/block-decoration-utils.js +9 -0
  10. package/dist/es2019/ui/consts.js +0 -8
  11. package/dist/esm/blockControlsPlugin.js +15 -1
  12. package/dist/esm/pm-plugins/main.js +56 -29
  13. package/dist/esm/ui/block-decoration-utils.js +9 -0
  14. package/dist/esm/ui/consts.js +0 -8
  15. package/dist/types/blockControlsPluginType.d.ts +20 -2
  16. package/dist/types/index.d.ts +1 -1
  17. package/dist/types/pm-plugins/main.d.ts +3 -3
  18. package/dist/types/ui/block-decoration-utils.d.ts +6 -0
  19. package/dist/types/ui/consts.d.ts +0 -8
  20. package/dist/types-ts4.5/blockControlsPluginType.d.ts +20 -2
  21. package/dist/types-ts4.5/index.d.ts +1 -1
  22. package/dist/types-ts4.5/pm-plugins/main.d.ts +3 -3
  23. package/dist/types-ts4.5/ui/block-decoration-utils.d.ts +6 -0
  24. package/dist/types-ts4.5/ui/consts.d.ts +0 -8
  25. package/package.json +6 -3
  26. package/dist/cjs/pm-plugins/decorations-remix-button.js +0 -67
  27. package/dist/cjs/ui/remix-button.js +0 -163
  28. package/dist/es2019/pm-plugins/decorations-remix-button.js +0 -56
  29. package/dist/es2019/ui/remix-button.js +0 -144
  30. package/dist/esm/pm-plugins/decorations-remix-button.js +0 -59
  31. package/dist/esm/ui/remix-button.js +0 -154
  32. package/dist/types/pm-plugins/decorations-remix-button.d.ts +0 -21
  33. package/dist/types/ui/remix-button.d.ts +0 -17
  34. package/dist/types-ts4.5/pm-plugins/decorations-remix-button.d.ts +0 -21
  35. package/dist/types-ts4.5/ui/remix-button.d.ts +0 -17
@@ -1,67 +0,0 @@
1
- "use strict";
2
-
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
- Object.defineProperty(exports, "__esModule", {
5
- value: true
6
- });
7
- exports.remixButtonDecoration = exports.findRemixButtonDecoration = void 0;
8
- var _react = require("react");
9
- var _uuid = _interopRequireDefault(require("uuid"));
10
- var _browserApis = require("@atlaskit/browser-apis");
11
- var _view = require("@atlaskit/editor-prosemirror/view");
12
- var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
13
- var _remixButton = require("../ui/remix-button");
14
- // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
15
-
16
- var TYPE_REMIX_BUTTON = 'REMIX_BUTTON';
17
- var findRemixButtonDecoration = exports.findRemixButtonDecoration = function findRemixButtonDecoration(decorations, from, to) {
18
- return decorations.find(from, to, function (spec) {
19
- return spec.type === TYPE_REMIX_BUTTON;
20
- });
21
- };
22
- /** Right-edge Remix button: same gutter as left controls (side: -4) but positioned at block right edge. */
23
- var remixButtonDecoration = exports.remixButtonDecoration = function remixButtonDecoration(_ref) {
24
- var api = _ref.api,
25
- formatMessage = _ref.formatMessage,
26
- rootPos = _ref.rootPos,
27
- anchorName = _ref.anchorName,
28
- nodeType = _ref.nodeType,
29
- nodeViewPortalProviderAPI = _ref.nodeViewPortalProviderAPI,
30
- rootAnchorName = _ref.rootAnchorName,
31
- rootNodeType = _ref.rootNodeType;
32
- // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
33
- var key = (0, _uuid.default)();
34
- var widgetSpec = {
35
- side: -4,
36
- type: TYPE_REMIX_BUTTON,
37
- destroy: function destroy(_) {
38
- if ((0, _platformFeatureFlags.fg)('platform_editor_fix_widget_destroy')) {
39
- nodeViewPortalProviderAPI.remove(key);
40
- }
41
- }
42
- };
43
- return _view.Decoration.widget(rootPos, function (view, getPos) {
44
- var doc = (0, _browserApis.getDocument)();
45
- if (!doc) {
46
- throw new Error('Document not available');
47
- }
48
- var element = doc.createElement('span');
49
- element.style.display = 'block';
50
- element.contentEditable = 'false';
51
- element.setAttribute('data-blocks-remix-button-container', 'true');
52
- element.setAttribute('data-testid', 'block-ctrl-remix-button-container');
53
- nodeViewPortalProviderAPI.render(function () {
54
- return /*#__PURE__*/(0, _react.createElement)(_remixButton.RemixButton, {
55
- api: api,
56
- getPos: getPos,
57
- formatMessage: formatMessage,
58
- view: view,
59
- nodeType: nodeType,
60
- anchorName: anchorName,
61
- rootAnchorName: rootAnchorName,
62
- rootNodeType: rootNodeType !== null && rootNodeType !== void 0 ? rootNodeType : nodeType
63
- });
64
- }, element, key, undefined, true);
65
- return element;
66
- }, widgetSpec);
67
- };
@@ -1,163 +0,0 @@
1
- "use strict";
2
-
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
- var _typeof = require("@babel/runtime/helpers/typeof");
5
- Object.defineProperty(exports, "__esModule", {
6
- value: true
7
- });
8
- exports.RemixButton = void 0;
9
- var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
10
- var _react = _interopRequireWildcard(require("react"));
11
- var _react2 = require("@emotion/react");
12
- var _bindEventListener = require("bind-event-listener");
13
- var _new = require("@atlaskit/button/new");
14
- var _hooks = require("@atlaskit/editor-common/hooks");
15
- var _randomize = _interopRequireDefault(require("@atlaskit/icon-lab/core/randomize"));
16
- var _dragHandlePositions = require("../pm-plugins/utils/drag-handle-positions");
17
- var _widgetPositions = require("../pm-plugins/utils/widget-positions");
18
- var _consts = require("./consts");
19
- var _anchorName = require("./utils/anchor-name");
20
- var _domAttrName = require("./utils/dom-attr-name");
21
- var _visibilityContainer = require("./visibility-container");
22
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
23
- /**
24
- * Remix block control: positioned at the right edge of the block.
25
- * Uses anchor(anchorName end) or getRightPositionForRootElement (same coordinate system
26
- * as left controls). The widget span has no position:relative so position:absolute
27
- * uses the same containing block as the node.
28
- */
29
- /* @jsxRuntime classic */
30
- /**
31
- * @jsxRuntime classic
32
- * @jsx jsx
33
- */
34
-
35
- // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
36
-
37
- var containerBaseStyles = (0, _react2.css)({
38
- position: 'absolute',
39
- zIndex: 100,
40
- display: 'flex',
41
- alignItems: 'center',
42
- justifyContent: 'center'
43
- });
44
- var RemixButton = exports.RemixButton = function RemixButton(_ref) {
45
- var view = _ref.view,
46
- api = _ref.api,
47
- getPos = _ref.getPos,
48
- anchorName = _ref.anchorName,
49
- rootAnchorName = _ref.rootAnchorName,
50
- rootNodeType = _ref.rootNodeType;
51
- var _useSharedPluginState = (0, _hooks.useSharedPluginStateWithSelector)(api, ['featureFlags'], function (states) {
52
- var _states$featureFlagsS;
53
- return {
54
- macroInteractionUpdates: (_states$featureFlagsS = states.featureFlagsState) === null || _states$featureFlagsS === void 0 ? void 0 : _states$featureFlagsS.macroInteractionUpdates
55
- };
56
- }),
57
- macroInteractionUpdates = _useSharedPluginState.macroInteractionUpdates;
58
- var _useState = (0, _react.useState)({
59
- display: 'none'
60
- }),
61
- _useState2 = (0, _slicedToArray2.default)(_useState, 2),
62
- positionStyles = _useState2[0],
63
- setPositionStyles = _useState2[1];
64
-
65
- // Same positioning pattern as quick insert / drag handle: anchor(start/end) or offset-based left/top
66
- var calculatePosition = (0, _react.useCallback)(function () {
67
- var _dom$offsetLeft;
68
- var safeAnchorName = (0, _anchorName.refreshAnchorName)({
69
- getPos: getPos,
70
- view: view,
71
- anchorName: rootAnchorName !== null && rootAnchorName !== void 0 ? rootAnchorName : anchorName
72
- });
73
- var dom = view.dom.querySelector("[".concat((0, _domAttrName.getAnchorAttrName)(), "=\"").concat(safeAnchorName, "\"]"));
74
- if (!dom) {
75
- return {
76
- display: 'none'
77
- };
78
- }
79
- var hasResizer = rootNodeType === 'table' || rootNodeType === 'mediaSingle';
80
- var isExtension = rootNodeType === 'extension' || rootNodeType === 'bodiedExtension';
81
- var isBlockCard = rootNodeType === 'blockCard';
82
- var isEmbedCard = rootNodeType === 'embedCard';
83
- var isMacroInteractionUpdates = macroInteractionUpdates && isExtension;
84
- var innerContainer = null;
85
- if (dom) {
86
- if (isEmbedCard) {
87
- innerContainer = dom.querySelector('.rich-media-item');
88
- } else if (hasResizer) {
89
- innerContainer = dom.querySelector('.resizer-item');
90
- } else if (isExtension) {
91
- innerContainer = dom.querySelector('.extension-container[data-layout]');
92
- } else if (isBlockCard) {
93
- innerContainer = dom.querySelector('.datasourceView-content-inner-wrap');
94
- }
95
- }
96
- var isEdgeCase = (hasResizer || isExtension || isEmbedCard || isBlockCard) && innerContainer;
97
-
98
- // Check anchor first (no reflow). Only call expensive getRightPositionForRootElement when fallback needed.
99
- var supportsAnchorRight = CSS.supports('left', "anchor(".concat(safeAnchorName, " end)")) && CSS.supports('top', "anchor(".concat(safeAnchorName, " start)"));
100
- if (supportsAnchorRight && !isEdgeCase) {
101
- return {
102
- left: "calc(anchor(".concat(safeAnchorName, " end) - ").concat(_consts.REMIX_BUTTON_DIMENSIONS.width, "px - ").concat((0, _consts.rootElementGap)(rootNodeType), "px + ").concat(_consts.REMIX_BUTTON_RIGHT_OFFSET, "px)"),
103
- top: "calc(anchor(".concat(safeAnchorName, " start) + ").concat((0, _consts.topPositionAdjustment)(rootNodeType), "px)"),
104
- height: "".concat(_consts.REMIX_BUTTON_DIMENSIONS.height, "px"),
105
- bottom: 'unset'
106
- };
107
- }
108
- // Fallback: offset-based (triggers reflow). When isEdgeCase add dom.offsetLeft (same as left controls).
109
- var rightEdgeLeft = (0, _widgetPositions.getRightPositionForRootElement)(dom, rootNodeType, _consts.REMIX_BUTTON_DIMENSIONS, innerContainer !== null && innerContainer !== void 0 ? innerContainer : undefined, isMacroInteractionUpdates);
110
- return {
111
- 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(_consts.REMIX_BUTTON_RIGHT_OFFSET, "px)") : "calc(".concat(rightEdgeLeft, " + ").concat(_consts.REMIX_BUTTON_RIGHT_OFFSET, "px)"),
112
- top: (0, _dragHandlePositions.getTopPosition)(dom, rootNodeType),
113
- height: "".concat(_consts.REMIX_BUTTON_DIMENSIONS.height, "px"),
114
- bottom: 'unset'
115
- };
116
- }, [view, getPos, anchorName, rootAnchorName, rootNodeType, macroInteractionUpdates]);
117
-
118
- // Recompute button position on mount and when extension/embedCard layout changes (e.g. expand/collapse).
119
- // For extension/embedCard we listen to transitionend so position updates after CSS transitions finish.
120
- (0, _react.useEffect)(function () {
121
- var cleanUpTransitionListener;
122
- if (rootNodeType === 'extension' || rootNodeType === 'embedCard') {
123
- var anchorDom = view.dom.querySelector("[".concat((0, _domAttrName.getAnchorAttrName)(), "=\"").concat(rootAnchorName !== null && rootAnchorName !== void 0 ? rootAnchorName : anchorName, "\"]"));
124
- if (anchorDom) {
125
- cleanUpTransitionListener = (0, _bindEventListener.bind)(anchorDom, {
126
- type: 'transitionend',
127
- listener: function listener() {
128
- return setPositionStyles(calculatePosition());
129
- }
130
- });
131
- }
132
- }
133
- var id = requestAnimationFrame(function () {
134
- return setPositionStyles(calculatePosition());
135
- });
136
- return function () {
137
- var _cleanUpTransitionLis;
138
- cancelAnimationFrame(id);
139
- (_cleanUpTransitionLis = cleanUpTransitionListener) === null || _cleanUpTransitionLis === void 0 || _cleanUpTransitionLis();
140
- };
141
- }, [calculatePosition, view.dom, rootAnchorName, rootNodeType, anchorName]);
142
- return (0, _react2.jsx)(_visibilityContainer.VisibilityContainer, {
143
- api: api
144
- }, (0, _react2.jsx)("div", {
145
- css: containerBaseStyles
146
- // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Dynamic positioning (left, top, height) calculated at runtime
147
- ,
148
- style: positionStyles,
149
- "data-testid": "block-ctrl-remix-button"
150
- }, (0, _react2.jsx)(_new.IconButton, {
151
- spacing: "compact",
152
- appearance: "subtle",
153
- label: "Remix",
154
- icon: function icon() {
155
- return (0, _react2.jsx)(_randomize.default, {
156
- label: ""
157
- });
158
- },
159
- onMouseDown: function onMouseDown(e) {
160
- return e.preventDefault();
161
- }
162
- })));
163
- };
@@ -1,56 +0,0 @@
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
- const TYPE_REMIX_BUTTON = 'REMIX_BUTTON';
9
- export const findRemixButtonDecoration = (decorations, from, to) => {
10
- return decorations.find(from, to, spec => spec.type === TYPE_REMIX_BUTTON);
11
- };
12
- /** Right-edge Remix button: same gutter as left controls (side: -4) but positioned at block right edge. */
13
- export const remixButtonDecoration = ({
14
- api,
15
- formatMessage,
16
- rootPos,
17
- anchorName,
18
- nodeType,
19
- nodeViewPortalProviderAPI,
20
- rootAnchorName,
21
- rootNodeType
22
- }) => {
23
- // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
24
- const key = uuid();
25
- const widgetSpec = {
26
- side: -4,
27
- type: TYPE_REMIX_BUTTON,
28
- destroy: _ => {
29
- if (fg('platform_editor_fix_widget_destroy')) {
30
- nodeViewPortalProviderAPI.remove(key);
31
- }
32
- }
33
- };
34
- return Decoration.widget(rootPos, (view, getPos) => {
35
- const doc = getDocument();
36
- if (!doc) {
37
- throw new Error('Document not available');
38
- }
39
- const element = doc.createElement('span');
40
- element.style.display = 'block';
41
- element.contentEditable = 'false';
42
- element.setAttribute('data-blocks-remix-button-container', 'true');
43
- element.setAttribute('data-testid', 'block-ctrl-remix-button-container');
44
- nodeViewPortalProviderAPI.render(() => /*#__PURE__*/createElement(RemixButton, {
45
- api,
46
- getPos,
47
- formatMessage,
48
- view,
49
- nodeType,
50
- anchorName,
51
- rootAnchorName,
52
- rootNodeType: rootNodeType !== null && rootNodeType !== void 0 ? rootNodeType : nodeType
53
- }), element, key, undefined, true);
54
- return element;
55
- }, widgetSpec);
56
- };
@@ -1,144 +0,0 @@
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
- };
@@ -1,59 +0,0 @@
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
- };
@@ -1,154 +0,0 @@
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
- };
@@ -1,21 +0,0 @@
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 {};