UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

267 lines (256 loc) 18.2 kB
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; import memoizeOne from 'memoize-one'; import { isMultiBlockSelection } from '@atlaskit/editor-common/selection'; import { areToolbarFlagsEnabled } from '@atlaskit/editor-common/toolbar-flag-check'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { getAnchorAttrName, getTypeNameAttrName, getTypeNameFromDom, NODE_ANCHOR_ATTR_NAME } from '../ui/utils/dom-attr-name'; import { IGNORE_NODE_DESCENDANTS_ADVANCED_LAYOUT, IGNORE_NODES_NEXT } from './decorations-anchor'; import { selectionPreservationPluginKey } from './selection-preservation/plugin-key'; var isEmptyNestedParagraphOrHeading = function isEmptyNestedParagraphOrHeading(target) { if (target instanceof HTMLHeadingElement || target instanceof HTMLParagraphElement) { var _target$parentElement; return !((_target$parentElement = target.parentElement) !== null && _target$parentElement !== void 0 && _target$parentElement.classList.contains('ProseMirror')) && target.textContent === ''; } return false; }; var getNodeSelector = function getNodeSelector(ignoreNodes, ignoreNodeDescendants) { var baseSelector = "[".concat(NODE_ANCHOR_ATTR_NAME, "]"); if (ignoreNodes.length === 0 && ignoreNodeDescendants.length === 0) { return baseSelector; } var ignoreNodeSelectorList = ignoreNodes.map(function (node) { return "[data-prosemirror-node-name=\"".concat(node, "\"]"); }); var ignoreNodeDescendantsSelectorList = ignoreNodeDescendants.map(function (node) { if (node === 'table') { // Special case for table to exclude its direct descendants return ["[data-prosemirror-node-name=\"tableCell\"] > [data-node-anchor]", "[data-prosemirror-node-name=\"tableCell\"] > *:not([data-node-anchor]) > [data-node-anchor]", "[data-prosemirror-node-name=\"tableHeader\"] > [data-node-anchor]", "[data-prosemirror-node-name=\"tableHeader\"] > *:not([data-node-anchor]) > [data-node-anchor]"]; } return "[data-prosemirror-node-name=\"".concat(node, "\"] [data-node-anchor]"); }); var ignoreSelector = [].concat(_toConsumableArray(ignoreNodeSelectorList), _toConsumableArray(ignoreNodeDescendantsSelectorList.flat()), ['[data-prosemirror-node-inline="true"]']).join(', '); return "".concat(baseSelector, ":not(").concat(ignoreSelector, ")"); }; var getDefaultNodeSelector = memoizeOne(function () { // we don't show handler for node nested in table return getNodeSelector([].concat(_toConsumableArray(IGNORE_NODES_NEXT), ['media']), [].concat(_toConsumableArray(IGNORE_NODE_DESCENDANTS_ADVANCED_LAYOUT), ['table'])); }); // Block marks (e.g. font-size) wrap paragraphs in an extra div, making parentRootElement // the mark wrapper instead of the panel content container. When that happens, recalculate // the index relative to the panel content so the first-child check stays accurate. var getBlockMarkPanelIndexAdjustment = function getBlockMarkPanelIndexAdjustment(parentRootElement, index) { if (expValEquals('platform_editor_small_font_size', 'isEnabled', true) && parentRootElement.classList.contains('fabric-editor-block-mark') && parentRootElement.parentElement) { return Array.from(parentRootElement.parentElement.childNodes).indexOf(parentRootElement); } return index; }; export var handleMouseOver = function handleMouseOver(view, event, api) { var _api$blockControls, _api$editorDisabled, _api$editorViewMode, _api$blockControls$sh, _api$blockControls2, _target$classList; var _ref = (api === null || api === void 0 || (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.sharedState.currentState()) || {}, isDragging = _ref.isDragging, activeNode = _ref.activeNode, isMenuOpen = _ref.isMenuOpen, originalAnchorName = _ref.menuTriggerBy, blockMenuOptions = _ref.blockMenuOptions; var _ref2 = (api === null || api === void 0 || (_api$editorDisabled = api.editorDisabled) === null || _api$editorDisabled === void 0 ? void 0 : _api$editorDisabled.sharedState.currentState()) || { editorDisabled: false }, editorDisabled = _ref2.editorDisabled; var editorViewMode = api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode; var isViewMode = editorViewMode === 'view'; var toolbarFlagsEnabled = areToolbarFlagsEnabled(Boolean(api === null || api === void 0 ? void 0 : api.toolbar)); // We shouldn't be firing mouse over transactions when the editor is disabled, // except in view mode when right-side controls are enabled (show controls on block hover) var rightSideControlsEnabled = (_api$blockControls$sh = api === null || api === void 0 || (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 || (_api$blockControls2 = _api$blockControls2.sharedState.currentState()) === null || _api$blockControls2 === void 0 ? void 0 : _api$blockControls2.rightSideControlsEnabled) !== null && _api$blockControls$sh !== void 0 ? _api$blockControls$sh : false; if (editorDisabled && (!isViewMode || !(rightSideControlsEnabled && fg('confluence_remix_button_right_side_block_fg')))) { return false; } // If the editor view is not in focus when the block menu is open, do not update the drag handle if (!view.hasFocus() && isMenuOpen && editorExperiment('platform_editor_block_menu', true, { exposure: true })) { return false; } // Most mouseover events don't fire during drag but some can slip through // when the drag begins. This prevents those. if (isDragging) { return false; } // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting var target = event.target; var isNativeAnchorSupported = expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true); if (target !== null && target !== void 0 && (_target$classList = target.classList) !== null && _target$classList !== void 0 && _target$classList.contains('ProseMirror')) { return false; } var rootElement = target === null || target === void 0 ? void 0 : target.closest(isNativeAnchorSupported ? getDefaultNodeSelector() : "[data-drag-handler-anchor-name]"); // When hovering over the right-edge button (rendered in a portal outside the block), resolve the // block from the container's anchor so activeNode stays set and the button remains visible. if (!rootElement && rightSideControlsEnabled && fg('confluence_remix_button_right_side_block_fg')) { var rightEdgeContainer = target === null || target === void 0 ? void 0 : target.closest('[data-blocks-right-edge-button-container]'); if (rightEdgeContainer) { var anchor = rightEdgeContainer.getAttribute('data-blocks-right-edge-button-anchor'); if (anchor) { rootElement = view.dom.querySelector("[".concat(getAnchorAttrName(), "=\"").concat(CSS.escape(anchor), "\"]")); } } } if (rootElement) { var _rootElement$parentEl; // We want to exlude handles from showing for empty paragraph and heading nodes if (isEmptyNestedParagraphOrHeading(rootElement)) { return false; } if (rootElement.getAttribute(getTypeNameAttrName()) === 'media' && editorExperiment('advanced_layouts', true) && !isNativeAnchorSupported) { rootElement = rootElement.closest("[".concat(getAnchorAttrName(), "][").concat(getTypeNameAttrName(), "=\"mediaSingle\"]")); if (!rootElement) { return false; } } var parentElement = (_rootElement$parentEl = rootElement.parentElement) === null || _rootElement$parentEl === void 0 ? void 0 : _rootElement$parentEl.closest("[".concat(getAnchorAttrName(), "]")); var parentElementType = isNativeAnchorSupported ? getTypeNameFromDom(parentElement) : parentElement === null || parentElement === void 0 ? void 0 : parentElement.getAttribute('data-drag-handler-node-type'); if (editorExperiment('advanced_layouts', true)) { // We want to exclude handles from showing for direct descendant of table nodes (i.e. nodes in cells) if (parentElement && parentElementType === 'table') { rootElement = parentElement; } else if (parentElement && parentElementType === 'tableRow') { var _parentElement$parent; var grandparentElement = parentElement === null || parentElement === void 0 || (_parentElement$parent = parentElement.parentElement) === null || _parentElement$parent === void 0 ? void 0 : _parentElement$parent.closest("[".concat(getAnchorAttrName(), "]")); var grandparentElementType = isNativeAnchorSupported ? getTypeNameFromDom(grandparentElement) : grandparentElement === null || grandparentElement === void 0 ? void 0 : grandparentElement.getAttribute('data-drag-handler-node-type'); if (grandparentElement && grandparentElementType === 'table') { rootElement = grandparentElement; } } } else { // We want to exclude handles from showing for direct descendant of table nodes (i.e. nodes in cells) if (parentElement && parentElementType === 'table') { rootElement = parentElement; } } // When platform_editor_fix_selection_wrapped_media_embed is enabled, a wrapped mediaSingle/embedCard and the // paragraph(s) it floats next to both have anchor decorations. If the hovered rootElement // is a paragraph whose adjacent DOM sibling is a wrapped mediaSingle/embedCard with an // anchor, redirect the handle to the media node so only one handle shows (ED-26959). // We check previousElementSibling (wrap-left: media precedes text in DOM) and // nextElementSibling (wrap-right: media follows text in DOM). // This must happen before anchorName is read so the correct node's handle is shown. if (editorExperiment('platform_editor_fix_selection_wrapped_media_embed', true, { exposure: true })) { var _rootNodeType = isNativeAnchorSupported ? getTypeNameFromDom(rootElement) : rootElement.getAttribute('data-drag-handler-node-type'); if (_rootNodeType === 'paragraph') { var isWrappedMediaEmbedSibling = function isWrappedMediaEmbedSibling(sibling) { if (!sibling || !sibling.getAttribute(getAnchorAttrName())) { return false; } var siblingType = isNativeAnchorSupported ? getTypeNameFromDom(sibling) : sibling.getAttribute('data-drag-handler-node-type'); var siblingLayout = sibling.getAttribute('layout') || ''; return (siblingType === 'mediaSingle' || siblingType === 'embedCard') && ['wrap-left', 'wrap-right'].includes(siblingLayout); }; var prevSibling = rootElement.previousElementSibling; var nextSibling = rootElement.nextElementSibling; if (prevSibling && isWrappedMediaEmbedSibling(prevSibling)) { rootElement = prevSibling; } else if (nextSibling && isWrappedMediaEmbedSibling(nextSibling)) { rootElement = nextSibling; } } } var anchorName; if (expValEquals('platform_editor_native_anchor_with_dnd', 'isEnabled', true)) { anchorName = rootElement.getAttribute(getAnchorAttrName()); // don't show handles if we can't find an anchor if (!anchorName) { return false; } } else { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion anchorName = rootElement.getAttribute(getAnchorAttrName()); } // No need to update handle position if its already there if ((activeNode === null || activeNode === void 0 ? void 0 : activeNode.anchorName) === anchorName) { return false; } if (!editorExperiment('platform_editor_fix_selection_wrapped_media_embed', true, { exposure: true })) { // We want to exclude handles from showing for wrapped nodes // TODO: ED-26959 - We should be able remove these check if we decided to // go we not decoration for wrapped image solution. if (['wrap-right', 'wrap-left'].includes(rootElement.getAttribute('layout') || '')) { return false; } } var parentRootElement = rootElement.parentElement; var pos; if (parentRootElement) { var _parentRootElement$ch; var childNodes = Array.from(parentRootElement.childNodes); var index = childNodes.indexOf(rootElement); pos = view.posAtDOM(parentRootElement, index); var panelIndex = getBlockMarkPanelIndexAdjustment(parentRootElement, index); // We want to exlude handles showing for first element in a Panel, ignoring widgets like gapcursor var firstChildIsWidget = parentRootElement === null || parentRootElement === void 0 || (_parentRootElement$ch = parentRootElement.children[0]) === null || _parentRootElement$ch === void 0 ? void 0 : _parentRootElement$ch.classList.contains('ProseMirror-widget'); if (parentElement && parentElementType === 'panel' && !parentElement.classList.contains('ak-editor-panel__no-icon') && (panelIndex === 0 || firstChildIsWidget && panelIndex === 1)) { return false; } } else { pos = view.posAtDOM(rootElement, 0); } if (parentRootElement && parentRootElement.getAttribute('data-layout-section') === 'true' && parentRootElement.querySelectorAll('[data-layout-column]').length === 1 && editorExperiment('advanced_layouts', true)) { // Don't show drag handle for layout column in a single column layout return false; } var targetPos = view.state.doc.resolve(pos).pos; var rootAnchorName; var rootNodeType; var rootPos; // platform_editor_controls note: enables quick insert if (toolbarFlagsEnabled) { rootPos = view.state.doc.resolve(pos).before(1); if (targetPos !== rootPos) { var rootDOM = view.nodeDOM(rootPos); if (rootDOM instanceof HTMLElement) { var _rootDOM$getAttribute; rootAnchorName = (_rootDOM$getAttribute = rootDOM.getAttribute(getAnchorAttrName())) !== null && _rootDOM$getAttribute !== void 0 ? _rootDOM$getAttribute : undefined; rootNodeType = isNativeAnchorSupported ? getTypeNameFromDom(rootDOM) : rootDOM.getAttribute('data-drag-handler-node-type'); } } } var nodeType = isNativeAnchorSupported ? getTypeNameFromDom(rootElement) : rootElement.getAttribute('data-drag-handler-node-type'); if (nodeType) { // platform_editor_controls note: enables quick insert if (toolbarFlagsEnabled) { if (editorExperiment('platform_editor_block_menu', true)) { var _selectionPreservatio; var preservedSelection = (_selectionPreservatio = selectionPreservationPluginKey.getState(view.state)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection; var selection = preservedSelection || view.state.selection; var isMultipleSelected = selection && isMultiBlockSelection(selection); // Only execute when selection is not a multi-selection, block menu is open, and menu is opened via keyboard // as when it is a multi-selection, the showDragHandleAt command interfere with selection // sometimes makes the multi-selection not continous after block menu is opened with keyboard if (!(isMultipleSelected && isMenuOpen && blockMenuOptions !== null && blockMenuOptions !== void 0 && blockMenuOptions.openedViaKeyboard)) { var _api$core, _api$blockControls3; api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(api === null || api === void 0 || (_api$blockControls3 = api.blockControls) === null || _api$blockControls3 === void 0 ? void 0 : _api$blockControls3.commands.showDragHandleAt(targetPos, anchorName, nodeType, undefined, rootPos !== null && rootPos !== void 0 ? rootPos : targetPos, rootAnchorName !== null && rootAnchorName !== void 0 ? rootAnchorName : anchorName, rootNodeType !== null && rootNodeType !== void 0 ? rootNodeType : nodeType)); } } else { var _api$core2, _api$blockControls4; api === null || api === void 0 || (_api$core2 = api.core) === null || _api$core2 === void 0 || _api$core2.actions.execute(api === null || api === void 0 || (_api$blockControls4 = api.blockControls) === null || _api$blockControls4 === void 0 ? void 0 : _api$blockControls4.commands.showDragHandleAt(targetPos, anchorName, nodeType, undefined, rootPos !== null && rootPos !== void 0 ? rootPos : targetPos, rootAnchorName !== null && rootAnchorName !== void 0 ? rootAnchorName : anchorName, rootNodeType !== null && rootNodeType !== void 0 ? rootNodeType : nodeType)); } } else { var _api$core3, _api$blockControls5; api === null || api === void 0 || (_api$core3 = api.core) === null || _api$core3 === void 0 || _api$core3.actions.execute(api === null || api === void 0 || (_api$blockControls5 = api.blockControls) === null || _api$blockControls5 === void 0 ? void 0 : _api$blockControls5.commands.showDragHandleAt(targetPos, anchorName, nodeType)); } if (editorExperiment('platform_editor_block_menu', true)) { var _api$userIntent; if (isMenuOpen && originalAnchorName && (api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 || (_api$userIntent = _api$userIntent.sharedState.currentState()) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.currentUserIntent) === 'blockMenuOpen') { var _api$core4, _api$blockControls6; api === null || api === void 0 || (_api$core4 = api.core) === null || _api$core4 === void 0 || _api$core4.actions.execute(api === null || api === void 0 || (_api$blockControls6 = api.blockControls) === null || _api$blockControls6 === void 0 ? void 0 : _api$blockControls6.commands.toggleBlockMenu()); } } } } };