@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
181 lines (180 loc) • 8.53 kB
JavaScript
import { useEffect } from 'react';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { startMeasure } from '@atlaskit/editor-common/performance-measures';
import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/adapter/element-adapter';
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/utils/set-custom-native-drag-preview';
import { defaultActiveAnchorTracker } from '../pm-plugins/utils/active-anchor-tracker';
import { getMultiSelectAnalyticsAttributes } from '../pm-plugins/utils/analytics';
import { isCollapsedHeading, prepareCollapsedHeadingSelection } from '../pm-plugins/utils/collapsed-heading';
import { isHandleCorrelatedToSelection, selectNode } from '../pm-plugins/utils/getSelection';
import { getNodeMarginsForDragPreview, getNodeSpacingForDragPreview } from './block-controls-surface-drag-handle-utils';
import { dragPreview } from './drag-preview';
const EDITOR_BLOCKS_DRAG_INIT = 'Editor Blocks Drag Initialization Time';
/**
* Registry surface drag source. This intentionally duplicates the stable widget handle's drag
* contract so the legacy implementation remains byte-for-byte unchanged during migration.
*/
export const useBlockControlsSurfaceDragSource = ({
api,
elementRef,
getAnchorName,
getPos,
nodeType,
start,
view
}) => {
useEffect(() => {
const element = elementRef.current;
if (!element || !view) {
return;
}
return draggable({
element,
getInitialData: () => ({
type: 'element',
start
}),
onGenerateDragPreview: ({
nativeSetDragImage
}) => {
var _api$core, _api$blockControls$sh;
api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
tr
}) => {
const handlePos = getPos();
if (typeof handlePos !== 'number') {
return tr;
}
if (isExperimentEnabled('platform_editor_collapsible_headings') && isCollapsedHeading(api, handlePos)) {
prepareCollapsedHeadingSelection({
api,
expand: false,
headingPos: handlePos,
tr
});
return tr;
}
if (!tr.selection.empty && isHandleCorrelatedToSelection(view.state, tr.selection, handlePos)) {
var _api$blockControls;
api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.commands.setMultiSelectPositions()({
tr
});
} else {
tr = selectNode(tr, handlePos, nodeType, api);
}
return tr;
});
const startPos = getPos();
const {
doc,
selection
} = view.state;
let sliceFrom = selection.from;
let sliceTo = selection.to;
const multiSelect = api === null || api === void 0 ? void 0 : (_api$blockControls$sh = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh === void 0 ? void 0 : _api$blockControls$sh.multiSelectDnD;
if (multiSelect) {
sliceFrom = Math.min(multiSelect.anchor, multiSelect.head);
sliceTo = Math.max(multiSelect.anchor, multiSelect.head);
}
const expandedSlice = doc.slice(sliceFrom, sliceTo);
const isDraggingMultipleNodes = startPos !== undefined && startPos >= sliceFrom && startPos < sliceTo && expandedSlice.content.childCount > 1;
setCustomNativeDragPreview({
getOffset: () => {
if (!isDraggingMultipleNodes || startPos === undefined) {
return {
x: 0,
y: 0
};
}
const domAtPos = view.domAtPos.bind(view);
let heightBeforeHandle = 0;
let nodeStart = sliceFrom;
let activeNodeMarginTop = 0;
for (let index = 0; index < expandedSlice.content.childCount; index++) {
var _node$nodeSize;
const node = expandedSlice.content.maybeChild(index);
const nodeEnd = nodeStart + ((_node$nodeSize = node === null || node === void 0 ? void 0 : node.nodeSize) !== null && _node$nodeSize !== void 0 ? _node$nodeSize : 0);
if (nodeEnd <= startPos) {
const nodeElement = findDomRefAtPos(nodeStart, domAtPos);
const margins = getNodeMarginsForDragPreview(node);
heightBeforeHandle += (nodeElement instanceof HTMLElement ? nodeElement.offsetHeight : 0) + margins.top + margins.bottom;
} else {
activeNodeMarginTop = getNodeMarginsForDragPreview(node).top;
break;
}
nodeStart = nodeEnd;
}
return {
x: 0,
y: heightBeforeHandle + activeNodeMarginTop
};
},
render: ({
container
}) => {
if (!isDraggingMultipleNodes && startPos !== undefined) {
const dom = findDomRefAtPos(startPos, view.domAtPos.bind(view));
return dom instanceof HTMLElement ? dragPreview(container, {
dom,
nodeType
}) : undefined;
}
const previewContent = [];
expandedSlice.content.descendants((node, pos) => {
const dom = findDomRefAtPos(sliceFrom + pos, view.domAtPos.bind(view));
if (dom instanceof HTMLElement) {
previewContent.push({
dom,
nodeSpacing: getNodeSpacingForDragPreview(node),
nodeType: node.type.name
});
}
return false;
});
return previewContent.length > 0 ? dragPreview(container, previewContent) : undefined;
},
nativeSetDragImage
});
},
onDragStart: () => {
var _api$core2;
if (start === undefined) {
return;
}
// This handle is outside ProseMirror, so it owns the side effects normally started by PM.
startMeasure(EDITOR_BLOCKS_DRAG_INIT);
defaultActiveAnchorTracker.reset();
api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
tr
}) => {
var _api$blockControls$sh2, _resolvedMovingNode$n, _api$blockControls2, _api$analytics;
const resolvedMovingNode = tr.doc.resolve(start);
const multiSelect = api === null || api === void 0 ? void 0 : (_api$blockControls$sh2 = api.blockControls.sharedState.currentState()) === null || _api$blockControls$sh2 === void 0 ? void 0 : _api$blockControls$sh2.multiSelectDnD;
const attributes = multiSelect ? getMultiSelectAnalyticsAttributes(tr, multiSelect.anchor, multiSelect.head) : {
hasSelectedMultipleNodes: false,
nodeTypes: (_resolvedMovingNode$n = resolvedMovingNode.nodeAfter) === null || _resolvedMovingNode$n === void 0 ? void 0 : _resolvedMovingNode$n.type.name
};
api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : _api$blockControls2.commands.setNodeDragged(getPos, getAnchorName(), nodeType)({
tr
});
tr.setMeta('scrollIntoView', false);
api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.attachAnalyticsEvent({
action: ACTION.DRAGGED,
actionSubject: ACTION_SUBJECT.ELEMENT,
actionSubjectId: ACTION_SUBJECT_ID.ELEMENT_DRAG_HANDLE,
attributes: {
hasSelectedMultipleNodes: attributes.hasSelectedMultipleNodes,
nodeDepth: resolvedMovingNode.depth,
nodeTypes: attributes.nodeTypes || ''
},
eventType: EVENT_TYPE.UI
})(tr);
return tr;
});
view.focus();
}
});
}, [api, elementRef, getAnchorName, getPos, nodeType, start, view]);
};