@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
267 lines (256 loc) • 18.2 kB
JavaScript
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());
}
}
}
}
};