UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

395 lines (387 loc) 23.5 kB
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 })); };