@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
129 lines (127 loc) • 8.61 kB
JavaScript
import { getNodeIdProvider } from '@atlaskit/editor-common/node-anchor-provider';
import { fg } from '@atlaskit/platform-feature-flags/fg';
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
import { getNodeTypeWithLevel } from './decorations-common';
const OPAQUE_BLOCKS = new Set(['bulletList', 'orderedList', 'taskList', 'decisionList', 'mediaSingle']);
const EXCLUDED_BLOCKS = new Set(['listItem', 'tableRow', 'tableCell', 'tableHeader', 'caption']);
const TABLE_CELLS = new Set(['tableCell', 'tableHeader']);
const isPanelNodeTypeName = nodeTypeName => nodeTypeName === 'panel' || nodeTypeName === 'panel_c1' && expValEqualsNoExposure('platform_editor_controls_reliable_anchor', 'isEnabled', true) && fg('platform_editor_controls_reliable_anchor_patch_1');
const redirectParagraphToWrappedMedia = (view, $target, targetPosition) => {
const node = view.state.doc.nodeAt(targetPosition);
if (!node || node.type.name !== 'paragraph') {
return {
position: targetPosition,
node
};
}
const index = $target.index();
for (const siblingIndex of [index - 1, index + 1]) {
if (siblingIndex < 0 || siblingIndex >= $target.parent.childCount) {
continue;
}
const sibling = $target.parent.child(siblingIndex);
if ((sibling.type.name === 'mediaSingle' || sibling.type.name === 'embedCard') && (sibling.attrs.layout === 'wrap-left' || sibling.attrs.layout === 'wrap-right')) {
return {
position: siblingIndex < index ? targetPosition - sibling.nodeSize : targetPosition + node.nodeSize,
node: sibling
};
}
}
return {
position: targetPosition,
node
};
};
/** Whether hovering must not show block controls under sparse surfaces. */
export const isSparseHoverSuppressed = (view, api) => {
var _api$limitedMode, _api$limitedMode$shar, _getNodeIdProvider, _api$typeAhead, _api$typeAhead$shared, _api$editorDisabled$s, _api$editorDisabled, _api$editorDisabled$s2, _api$editorViewMode, _api$editorViewMode$s, _api$showDiff$sharedS, _api$showDiff, _api$showDiff$sharedS2, _api$userIntent, _api$userIntent$share;
if (api !== null && api !== void 0 && (_api$limitedMode = api.limitedMode) !== null && _api$limitedMode !== void 0 && (_api$limitedMode$shar = _api$limitedMode.sharedState.currentState()) !== null && _api$limitedMode$shar !== void 0 && _api$limitedMode$shar.enabled || (_getNodeIdProvider = getNodeIdProvider(view)) !== null && _getNodeIdProvider !== void 0 && _getNodeIdProvider.isLimitedMode()) {
return true;
}
const controls = api === null || api === void 0 ? void 0 : api.blockControls.sharedState.currentState();
if (controls !== null && controls !== void 0 && controls.isDragging || api !== null && api !== void 0 && (_api$typeAhead = api.typeAhead) !== null && _api$typeAhead !== void 0 && (_api$typeAhead$shared = _api$typeAhead.sharedState.currentState()) !== null && _api$typeAhead$shared !== void 0 && _api$typeAhead$shared.isOpen) {
return true;
}
const editorDisabled = (_api$editorDisabled$s = api === null || api === void 0 ? void 0 : (_api$editorDisabled = api.editorDisabled) === null || _api$editorDisabled === void 0 ? void 0 : (_api$editorDisabled$s2 = _api$editorDisabled.sharedState.currentState()) === null || _api$editorDisabled$s2 === void 0 ? void 0 : _api$editorDisabled$s2.editorDisabled) !== null && _api$editorDisabled$s !== void 0 ? _api$editorDisabled$s : false;
const editorViewMode = api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode;
if (editorDisabled && (editorViewMode !== 'view' || !(controls !== null && controls !== void 0 && controls.rightSideControlsEnabled))) {
return true;
}
const isDisplayingDiff = (_api$showDiff$sharedS = api === null || api === void 0 ? void 0 : (_api$showDiff = api.showDiff) === null || _api$showDiff === void 0 ? void 0 : (_api$showDiff$sharedS2 = _api$showDiff.sharedState.currentState()) === null || _api$showDiff$sharedS2 === void 0 ? void 0 : _api$showDiff$sharedS2.isDisplayingChanges) !== null && _api$showDiff$sharedS !== void 0 ? _api$showDiff$sharedS : false;
return isDisplayingDiff || (api === null || api === void 0 ? void 0 : (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : (_api$userIntent$share = _api$userIntent.sharedState.currentState()) === null || _api$userIntent$share === void 0 ? void 0 : _api$userIntent$share.currentUserIntent) === 'reviewing';
};
/**
* Shows controls for the block that owns `target`, resolved from PM structure so the first hover
* can create its sparse candidate. Callers check `isSparseHoverSuppressed` first.
*/
export const handleSparseHoverTarget = (view, target, api) => {
var _$pos$nodeAfter, _controls$activeNode, _anchors$get, _anchors$get2;
if (!api || target === view.dom) {
return false;
}
const controls = api.blockControls.sharedState.currentState();
let position;
try {
position = view.posAtDOM(target, 0);
} catch {
return false;
}
const $pos = view.state.doc.resolve(position);
let targetPosition;
let opaque = false;
for (let depth = 1; depth <= $pos.depth; depth++) {
const node = $pos.node(depth);
if (!node.isBlock || EXCLUDED_BLOCKS.has(node.type.name)) {
continue;
}
// Legacy hover excludes direct cell children, but can target deeper blocks inside them.
if (!TABLE_CELLS.has($pos.node(depth - 1).type.name)) {
targetPosition = $pos.before(depth);
}
if (OPAQUE_BLOCKS.has(node.type.name)) {
opaque = true;
break;
}
}
if (!opaque && (_$pos$nodeAfter = $pos.nodeAfter) !== null && _$pos$nodeAfter !== void 0 && _$pos$nodeAfter.isBlock && !EXCLUDED_BLOCKS.has($pos.nodeAfter.type.name) && !TABLE_CELLS.has($pos.parent.type.name)) {
// Browser positions over non-editable node chrome can resolve to the first editable child.
// Keep the ancestor unless that child actually owns the hovered DOM target.
const nodeAfterDOM = view.nodeDOM(position);
if (nodeAfterDOM instanceof Element && nodeAfterDOM.contains(target)) {
targetPosition = position;
}
}
if (targetPosition === undefined) {
return false;
}
const initialNode = view.state.doc.nodeAt(targetPosition);
if (!initialNode) {
return false;
}
const redirected = redirectParagraphToWrappedMedia(view, view.state.doc.resolve(targetPosition), targetPosition);
targetPosition = redirected.position;
const node = redirected.node;
if (!node || (controls === null || controls === void 0 ? void 0 : (_controls$activeNode = controls.activeNode) === null || _controls$activeNode === void 0 ? void 0 : _controls$activeNode.pos) === targetPosition) {
return false;
}
const $target = view.state.doc.resolve(targetPosition);
if ($target.depth > 0 && (node.type.name === 'paragraph' || node.type.name === 'heading') && node.content.size === 0) {
return false;
}
const parentNode = $target.parent;
const parentPosition = $target.depth > 0 ? $target.before($target.depth) : undefined;
const parentDOM = parentPosition === undefined ? undefined : view.nodeDOM(parentPosition);
if (isPanelNodeTypeName(parentNode.type.name) && !(parentDOM instanceof HTMLElement && parentDOM.classList.contains('ak-editor-panel__no-icon')) && $target.index() === 0) {
return false;
}
const anchors = controls === null || controls === void 0 ? void 0 : controls.surfaceAnchors;
const anchorName = (_anchors$get = anchors === null || anchors === void 0 ? void 0 : anchors.get(targetPosition)) !== null && _anchors$get !== void 0 ? _anchors$get : api.core.actions.getAnchorIdForNode(node, targetPosition);
const rootPos = $target.depth > 0 ? $target.before(1) : targetPosition;
const rootNode = view.state.doc.nodeAt(rootPos);
const rootAnchorName = (_anchors$get2 = anchors === null || anchors === void 0 ? void 0 : anchors.get(rootPos)) !== null && _anchors$get2 !== void 0 ? _anchors$get2 : rootNode ? api.core.actions.getAnchorIdForNode(rootNode, rootPos) : undefined;
if (!anchorName || !rootNode) {
return false;
}
api.core.actions.execute(api.blockControls.commands.showDragHandleAt(targetPosition, anchorName, getNodeTypeWithLevel(node), undefined, rootPos, rootAnchorName !== null && rootAnchorName !== void 0 ? rootAnchorName : anchorName, getNodeTypeWithLevel(rootNode)));
return false;
};