@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
395 lines (387 loc) • 23.5 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
/**
* @jsxRuntime classic
* @jsx jsx
*/
import React, { useCallback, useEffect, useState } from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports
import { css, jsx } from '@emotion/react';
import { bind } from 'bind-event-listener';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
import { ToolTipContent } from '@atlaskit/editor-common/keymaps';
import { blockControlsMessages as messages } from '@atlaskit/editor-common/messages';
import { tableControlsSpacing } from '@atlaskit/editor-common/styles';
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
import { findParentNode, findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { relativeSizeToBaseFontSize } from '@atlaskit/editor-shared-styles';
import { CellSelection } from '@atlaskit/editor-tables/cell-selection';
import AddIcon from '@atlaskit/icon/core/add';
import { fg } from '@atlaskit/platform-feature-flags';
// eslint-disable-next-line @atlaskit/design-system/no-emotion-primitives -- to be migrated to @atlaskit/primitives/compiled – go/akcss
import { Box, Pressable, xcss } from '@atlaskit/primitives';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import Tooltip from '@atlaskit/tooltip';
import { getNodeTypeWithLevel } from '../pm-plugins/decorations-common';
import { getControlBottomCSSValue, getControlHeightCSSValue, getNodeHeight, getTopPosition, shouldBeSticky } from '../pm-plugins/utils/drag-handle-positions';
import { getLeftPositionForRootElement } from '../pm-plugins/utils/widget-positions';
import { QUICK_INSERT_DIMENSIONS, QUICK_INSERT_HEIGHT, QUICK_INSERT_LEFT_OFFSET, QUICK_INSERT_WIDTH, rootElementGap, STICKY_CONTROLS_TOP_MARGIN_FOR_STICKY_HEADER, topPositionAdjustment } from './consts';
import { refreshAnchorName } from './utils/anchor-name';
import { isInTextSelection, isNestedNodeSelected, isNonEditableBlock, isSelectionInNode } from './utils/document-checks';
import { getAnchorAttrName } from './utils/dom-attr-name';
import { createNewLine } from './utils/editor-commands';
import { VisibilityContainer } from './visibility-container';
var TEXT_PARENT_TYPES = ['paragraph', 'heading', 'blockquote', 'taskItem', 'decisionItem'];
var stickyButtonStyles = xcss({
top: '0',
position: 'sticky',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: "var(--ds-space-300, 24px)",
width: "var(--ds-space-300, 24px)",
border: 'none',
backgroundColor: 'color.background.neutral.subtle',
borderRadius: 'radius.full',
zIndex: 'card',
outline: 'none',
':hover': {
backgroundColor: 'color.background.neutral.subtle.hovered'
},
':active': {
backgroundColor: 'color.background.neutral.subtle.pressed'
},
':focus': {
outline: "var(--ds-border-width-focused, 2px)".concat(" solid ", "var(--ds-border-focused, #4688EC)")
}
});
// Calculate scaled dimensions based on the base font size using CSS calc()
// Default font size is 16px, scale proportionally
// Standard: 16px -> 24px x 24px, Dense: 13px -> ~18.5px x ~18.5px
var stickyButtonDenseModeStyles = xcss({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
height: relativeSizeToBaseFontSize(QUICK_INSERT_HEIGHT),
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
width: relativeSizeToBaseFontSize(QUICK_INSERT_WIDTH)
});
var containerStaticStyles = xcss({
position: 'absolute',
zIndex: 'card'
});
var tooltipContainerStyles = css({
top: '8px',
bottom: '-8px',
position: 'sticky',
display: 'block',
zIndex: 'card'
});
var tooltipContainerStylesStickyHeader = css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-quick-insert-container]:has(~ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: tableControlsSpacing
}
});
var tooltipContainerImprovedStylesStickyHeader = css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-quick-insert-container]:has(~ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: tableControlsSpacing
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-quick-insert-container]:has(~ [data-prosemirror-mark-name="fragment"] >[data-prosemirror-node-name="table"] tr.pm-table-row-native-sticky.pm-table-row-native-sticky-active) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: "".concat(STICKY_CONTROLS_TOP_MARGIN_FOR_STICKY_HEADER, "px")
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-quick-insert-container]:has(~ [data-prosemirror-node-name="table"] tr.pm-table-row-native-sticky.pm-table-row-native-sticky-active) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: "".concat(STICKY_CONTROLS_TOP_MARGIN_FOR_STICKY_HEADER, "px")
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-blocks-quick-insert-container]:has(~ [data-prosemirror-mark-name="fragment"] >[data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: tableControlsSpacing
}
});
// We need this to work around adjacent breakout marks wrapping the controls widget decorations
var tooltipContainerStylesStickyHeaderWithMarksFix = css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-quick-insert-container]):has(~ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: tableControlsSpacing
}
});
// We need this to work around adjacent breakout marks wrapping the controls widget decorations
var tooltipContainerStylesImprovedStickyHeaderWithMarksFix = css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-quick-insert-container]):has(~ [data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: tableControlsSpacing
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-quick-insert-container]):has(~ [data-prosemirror-mark-name="fragment"] >[data-prosemirror-node-name="table"] .pm-table-with-controls tr.sticky) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: tableControlsSpacing
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-quick-insert-container]):has(~ [data-prosemirror-node-name="table"] tr.pm-table-row-native-sticky.pm-table-row-native-sticky-active) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: "".concat(STICKY_CONTROLS_TOP_MARGIN_FOR_STICKY_HEADER, "px")
},
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors
'[data-prosemirror-mark-name="breakout"]:has([data-blocks-quick-insert-container]):has(~ [data-prosemirror-mark-name="fragment"] >[data-prosemirror-node-name="table"] tr.pm-table-row-native-sticky.pm-table-row-native-sticky-active) &': {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
top: "".concat(STICKY_CONTROLS_TOP_MARGIN_FOR_STICKY_HEADER, "px")
}
});
// TODO: ED-26959 - Share prop types between DragHandle - generic enough to create a type for block control decoration
export var TypeAheadControl = function TypeAheadControl(_ref) {
var view = _ref.view,
api = _ref.api,
formatMessage = _ref.formatMessage,
getPos = _ref.getPos,
anchorName = _ref.anchorName,
rootAnchorName = _ref.rootAnchorName,
rootNodeType = _ref.rootNodeType,
anchorRectCache = _ref.anchorRectCache;
var _useSharedPluginState = useSharedPluginStateWithSelector(api, ['featureFlags'], function (states) {
var _states$featureFlagsS;
return {
macroInteractionUpdates: (_states$featureFlagsS = states.featureFlagsState) === null || _states$featureFlagsS === void 0 ? void 0 : _states$featureFlagsS.macroInteractionUpdates
};
}),
macroInteractionUpdates = _useSharedPluginState.macroInteractionUpdates;
var _useState = useState({
display: 'none'
}),
_useState2 = _slicedToArray(_useState, 2),
positionStyles = _useState2[0],
setPositionStyles = _useState2[1];
// Adapted from `src/ui/drag-handle.tsx` as positioning logic is similar
// CHANGES - added an offset so quick insert button can be positioned beside drag handle
// CHANGES - removed `editorExperiment('nested-dnd', true)` check and rootNodeType calculation
// CHANGES - replace anchorName with rootAnchorName
// CHANGES - `removed editorExperiment('advanced_layouts', true) && isLayoutColumn` checks as quick insert button will not be positioned for layout column
var calculatePosition = useCallback(function () {
var supportsAnchor = CSS.supports('top', "anchor(".concat(rootAnchorName, " start)")) && CSS.supports('left', "anchor(".concat(rootAnchorName, " start)"));
var pos = getPos();
var node = pos !== undefined ? view.state.doc.nodeAt(pos) : undefined;
var safeAnchorName = refreshAnchorName({
getPos: getPos,
view: view,
anchorName: rootAnchorName
});
var dom = view.dom.querySelector("[".concat(getAnchorAttrName(), "=\"").concat(safeAnchorName, "\"]"));
var hasResizer = rootNodeType === 'table' || rootNodeType === 'mediaSingle';
var isExtension = rootNodeType === 'extension' || rootNodeType === 'bodiedExtension';
var isBlockCard = rootNodeType === 'blockCard';
var isEmbedCard = rootNodeType === 'embedCard';
var isMacroInteractionUpdates = macroInteractionUpdates && isExtension;
var innerContainer = null;
if (dom) {
if (isEmbedCard) {
innerContainer = dom.querySelector('.rich-media-item');
} else if (hasResizer) {
innerContainer = dom.querySelector('.resizer-item');
} else if (isExtension) {
innerContainer = dom.querySelector('.extension-container[data-layout]');
} else if (isBlockCard) {
//specific to datasource blockCard
innerContainer = dom.querySelector('.datasourceView-content-inner-wrap');
}
}
var isEdgeCase = (hasResizer || isExtension || isEmbedCard || isBlockCard) && innerContainer;
var isSticky = shouldBeSticky(rootNodeType);
var bottom = getControlBottomCSSValue(safeAnchorName || anchorName, isSticky, true);
if (supportsAnchor) {
return _objectSpread({
left: isEdgeCase ? "calc(anchor(".concat(safeAnchorName, " start) + ").concat(getLeftPositionForRootElement(dom, rootNodeType, QUICK_INSERT_DIMENSIONS, innerContainer, isMacroInteractionUpdates), " + -").concat(QUICK_INSERT_LEFT_OFFSET, "px)") : "calc(anchor(".concat(safeAnchorName, " start) - ").concat(QUICK_INSERT_DIMENSIONS.width, "px - ").concat(rootElementGap(rootNodeType), "px + -").concat(QUICK_INSERT_LEFT_OFFSET, "px)"),
// small text requires further tweaking to positioning, re-using existing methods to calculate to keep it unified with drag handle
top: "calc(anchor(".concat(safeAnchorName, " start) + ").concat(topPositionAdjustment(node && node.type.name === 'paragraph' && expValEquals('platform_editor_small_font_size', 'isEnabled', true) ? getNodeTypeWithLevel(node) : rootNodeType), "px)")
}, bottom);
}
// expensive, calls offsetHeight
var nodeHeight = getNodeHeight(dom, safeAnchorName || anchorName, anchorRectCache) || 0;
var height = getControlHeightCSSValue(nodeHeight, isSticky, true, "var(--ds-space-300, 24px)");
return _objectSpread({
left: isEdgeCase ? "calc(".concat((dom === null || dom === void 0 ? void 0 : dom.offsetLeft) || 0, "px + ").concat(getLeftPositionForRootElement(dom, rootNodeType, QUICK_INSERT_DIMENSIONS, innerContainer, isMacroInteractionUpdates), " + -").concat(QUICK_INSERT_LEFT_OFFSET, "px)") : "calc(".concat(getLeftPositionForRootElement(dom, rootNodeType, QUICK_INSERT_DIMENSIONS, innerContainer, isMacroInteractionUpdates), " + -").concat(QUICK_INSERT_LEFT_OFFSET, "px)"),
top: getTopPosition(dom, rootNodeType)
}, height);
}, [rootAnchorName, getPos, view, rootNodeType, macroInteractionUpdates, anchorName, anchorRectCache]);
useEffect(function () {
var cleanUpTransitionListener;
if (rootNodeType === 'extension' || rootNodeType === 'embedCard') {
var dom = view.dom.querySelector("[".concat(getAnchorAttrName(), "=\"").concat(rootAnchorName, "\"]"));
if (!dom) {
return;
}
cleanUpTransitionListener = bind(dom, {
type: 'transitionend',
listener: function listener() {
setPositionStyles(calculatePosition());
}
});
}
var calcPos = requestAnimationFrame(function () {
setPositionStyles(calculatePosition());
});
return function () {
var _cleanUpTransitionLis;
cancelAnimationFrame(calcPos);
(_cleanUpTransitionLis = cleanUpTransitionListener) === null || _cleanUpTransitionLis === void 0 || _cleanUpTransitionLis();
};
}, [calculatePosition, view.dom, rootAnchorName, rootNodeType]);
var handleQuickInsert = useCallback(function () {
var _api$quickInsert;
// if the selection is not within the node this decoration is rendered at
// then insert a newline and trigger quick insert
var start = getPos();
if (start !== undefined) {
// if the selection is not within the node this decoration is rendered at
// or the node is non-editable, then insert a newline and trigger quick insert
var isSelectionInsideNode = isSelectionInNode(start, view);
if (!isSelectionInsideNode || isNonEditableBlock(start, view)) {
api.core.actions.execute(createNewLine(start));
}
var codeBlock = view.state.schema.nodes.codeBlock;
var selection = view.state.selection;
var codeBlockParentNode = findParentNodeOfType(codeBlock)(selection);
if (codeBlockParentNode) {
// Slash command is not meant to be triggered inside code block, hence always insert slash in a new line following
api.core.actions.execute(createNewLine(codeBlockParentNode.pos));
} else if (isSelectionInsideNode) {
// text or element with be deselected and the / added immediately after the paragraph
// unless the selection is empty
var currentSelection = view.state.selection;
if (isInTextSelection(view) && currentSelection.from !== currentSelection.to) {
var currentParagraphNode = findParentNode(function (node) {
return TEXT_PARENT_TYPES.includes(node.type.name);
})(currentSelection);
if (currentParagraphNode) {
var newPos =
//if the current selection is selected from right to left, then set the selection to the start of the paragraph
currentSelection.anchor === currentSelection.to ? currentParagraphNode.pos : currentParagraphNode.pos + currentParagraphNode.node.nodeSize - 1;
api.core.actions.execute(function (_ref2) {
var tr = _ref2.tr;
tr.setSelection(TextSelection.create(view.state.selection.$from.doc, newPos));
return tr;
});
}
}
if (isNestedNodeSelected(view)) {
// if the nested selected node is non-editable, then insert a newline below the selected node
if (isNonEditableBlock(view.state.selection.from, view)) {
api.core.actions.execute(createNewLine(view.state.selection.from));
} else {
// otherwise need to force the selection to be at the start of the node, because
// prosemirror is keeping it as NodeSelection for nested nodes. Do this to keep it
// consistent NodeSelection for root level nodes.
api.core.actions.execute(function (_ref3) {
var tr = _ref3.tr;
createNewLine(view.state.selection.from)({
tr: tr
});
tr.setSelection(TextSelection.create(tr.doc, view.state.selection.from));
return tr;
});
}
}
if (currentSelection instanceof CellSelection) {
// find the last inline position in the selection
var lastInlinePosition = TextSelection.near(view.state.selection.$to, -1);
lastInlinePosition && api.core.actions.execute(function (_ref4) {
var tr = _ref4.tr;
if (!(lastInlinePosition instanceof TextSelection)) {
// this will create a new line after the node
createNewLine(lastInlinePosition.from)({
tr: tr
});
// this will find the next valid text position after the node
tr.setSelection(TextSelection.create(tr.doc, lastInlinePosition.to));
} else {
tr.setSelection(lastInlinePosition);
}
return tr;
});
}
}
}
(_api$quickInsert = api.quickInsert) === null || _api$quickInsert === void 0 || _api$quickInsert.actions.openTypeAhead('blockControl', true);
}, [api, getPos, view]);
var handleMouseDown = useCallback(function () {
var _api$typeAhead;
// close typeahead if it is open, must happen in mouseDown otherwise typeAhead popup will be dismissed and text is left
if ((_api$typeAhead = api.typeAhead) !== null && _api$typeAhead !== void 0 && _api$typeAhead.actions.isOpen(view.state)) {
api.typeAhead.actions.close({
insertCurrentQueryAsRawText: false
});
}
}, [api, view.state]);
var tooltipPressable = function tooltipPressable() {
return jsx(Tooltip, {
position: "top",
content: jsx(ToolTipContent, {
description: formatMessage(messages.insert)
})
}, jsx(Pressable, {
testId: "editor-quick-insert-button",
type: "button",
"aria-label": formatMessage(messages.insert)
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
xcss: [stickyButtonStyles, (expValEquals('confluence_compact_text_format', 'isEnabled', true) || expValEquals('cc_editor_ai_content_mode', 'variant', 'test') && fg('platform_editor_content_mode_button_mvp')) && stickyButtonDenseModeStyles],
onClick: handleQuickInsert,
onMouseDown: handleMouseDown
}, jsx(AddIcon, {
label: "add",
color: "var(--ds-icon-subtle, #505258)",
size: "small"
})));
};
return jsx(Box
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
, {
style: positionStyles
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
xcss: [containerStaticStyles]
}, jsx("span", {
css: [tooltipContainerStyles, expValEquals('platform_editor_table_sticky_header_improvements', 'cohort', 'test_with_overflow') && fg('platform_editor_table_sticky_header_patch_6') ? tooltipContainerImprovedStylesStickyHeader : tooltipContainerStylesStickyHeader, expValEquals('platform_editor_table_sticky_header_improvements', 'cohort', 'test_with_overflow') && fg('platform_editor_table_sticky_header_patch_6') ? tooltipContainerStylesImprovedStickyHeaderWithMarksFix : tooltipContainerStylesStickyHeaderWithMarksFix]
}, tooltipPressable()));
};
export var QuickInsertWithVisibility = function QuickInsertWithVisibility(_ref5) {
var view = _ref5.view,
api = _ref5.api,
formatMessage = _ref5.formatMessage,
getPos = _ref5.getPos,
nodeType = _ref5.nodeType,
anchorName = _ref5.anchorName,
rootAnchorName = _ref5.rootAnchorName,
rootNodeType = _ref5.rootNodeType,
anchorRectCache = _ref5.anchorRectCache;
var rightSideControlsEnabled = useSharedPluginStateWithSelector(api, ['blockControls'], function (states) {
var _states$blockControls, _states$blockControls2;
return {
rightSideControlsEnabled: (_states$blockControls = (_states$blockControls2 = states.blockControlsState) === null || _states$blockControls2 === void 0 ? void 0 : _states$blockControls2.rightSideControlsEnabled) !== null && _states$blockControls !== void 0 ? _states$blockControls : false
};
}).rightSideControlsEnabled;
return jsx(VisibilityContainer, {
api: api,
controlSide: rightSideControlsEnabled ? 'left' : undefined
}, jsx(TypeAheadControl, {
view: view,
api: api,
formatMessage: formatMessage,
getPos: getPos,
nodeType: nodeType,
anchorName: anchorName,
rootAnchorName: rootAnchorName,
rootNodeType: rootNodeType,
anchorRectCache: anchorRectCache
}));
};