@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
358 lines (353 loc) • 20.2 kB
JavaScript
import React from 'react';
import { expandSelectionBounds } from '@atlaskit/editor-common/selection';
import { areToolbarFlagsEnabled } from '@atlaskit/editor-common/toolbar-flag-check';
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
import { fg } from '@atlaskit/platform-feature-flags';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { handleKeyDownWithPreservedSelection } from './editor-commands/handle-key-down-with-preserved-selection';
import { mapPreservedSelection } from './editor-commands/map-preserved-selection';
import { moveNode } from './editor-commands/move-node';
import { moveNodeWithBlockMenu } from './editor-commands/move-node-with-block-menu';
import { moveToLayout } from './editor-commands/move-to-layout';
import { canMoveNodeUpOrDown } from './editor-commands/utils/move-node-utils';
import { firstNodeDecPlugin } from './pm-plugins/first-node-dec-plugin';
import { createInteractionTrackingPlugin, interactionTrackingPluginKey } from './pm-plugins/interaction-tracking/pm-plugin';
import { createPlugin, key } from './pm-plugins/main';
import { startPreservingSelection, stopPreservingSelection } from './pm-plugins/selection-preservation/editor-commands';
import { selectionPreservationPluginKey } from './pm-plugins/selection-preservation/plugin-key';
import { createSelectionPreservationPlugin } from './pm-plugins/selection-preservation/pm-plugin';
import { expandAndUpdateSelection } from './pm-plugins/utils/expand-and-update-selection';
import { selectNode } from './pm-plugins/utils/getSelection';
import { GlobalStylesWrapper } from './ui/global-styles';
export const blockControlsPlugin = ({
api,
config
}) => {
var _config$rightSideCont, _config$quickInsertBu;
const nodeDecorationRegistry = [];
const rightSideControlsEnabled = (_config$rightSideCont = config === null || config === void 0 ? void 0 : config.rightSideControlsEnabled) !== null && _config$rightSideCont !== void 0 ? _config$rightSideCont : false;
const quickInsertButtonEnabled = (_config$quickInsertBu = config === null || config === void 0 ? void 0 : config.quickInsertButtonEnabled) !== null && _config$quickInsertBu !== void 0 ? _config$quickInsertBu : true;
return {
name: 'blockControls',
actions: {
registerNodeDecoration: factory => {
nodeDecorationRegistry.push(factory);
},
unregisterNodeDecoration: type => {
const idx = nodeDecorationRegistry.findIndex(f => f.type === type);
if (idx !== -1) {
nodeDecorationRegistry.splice(idx, 1);
}
}
},
pmPlugins() {
const pmPlugins = [{
name: 'blockControlsPmPlugin',
plugin: ({
getIntl,
nodeViewPortalProviderAPI
}) => createPlugin(api, getIntl, nodeViewPortalProviderAPI, nodeDecorationRegistry, rightSideControlsEnabled, quickInsertButtonEnabled)
}];
if (editorExperiment('platform_editor_controls', 'variant1')) {
pmPlugins.push({
name: 'blockControlsInteractionTrackingPlugin',
plugin: () => createInteractionTrackingPlugin(rightSideControlsEnabled)
});
}
if (editorExperiment('platform_editor_block_menu', true)) {
pmPlugins.push({
name: 'blockControlsSelectionPreservationPlugin',
plugin: createSelectionPreservationPlugin(api)
});
}
// platform_editor_controls note: quick insert rendering fixes
if (areToolbarFlagsEnabled(Boolean(api === null || api === void 0 ? void 0 : api.toolbar))) {
pmPlugins.push({
name: 'firstNodeDec',
plugin: firstNodeDecPlugin
});
}
return pmPlugins;
},
commands: {
expandAndUpdateSelection: ({
startPos,
selection,
isShiftPressed,
nodeType
}) => ({
tr
}) => {
expandAndUpdateSelection({
tr,
selection,
startPos,
isShiftPressed,
nodeType,
api: api
});
return tr;
},
moveNode: moveNode(api),
moveToLayout: moveToLayout(api),
showDragHandleAt: (pos, anchorName, nodeType, handleOptions, rootPos, rootAnchorName, rootNodeType) => ({
tr
}) => {
const currMeta = tr.getMeta(key);
tr.setMeta(key, {
...currMeta,
activeNode: {
pos,
anchorName,
nodeType,
handleOptions,
rootPos,
rootAnchorName,
rootNodeType
}
});
return tr;
},
toggleBlockMenu: options => ({
tr
}) => {
var _api$userIntent, _api$userIntent$share, _api$blockControls, _api$blockControls$sh, _options$anchorName, _api$blockControls2, _api$blockControls2$s;
if (!editorExperiment('platform_editor_block_menu', true)) {
return tr;
}
const currMeta = tr.getMeta(key);
const currentUserIntent = 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;
const isMenuCurrentlyOpen = api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : (_api$blockControls$sh = _api$blockControls.sharedState.currentState()) === null || _api$blockControls$sh === void 0 ? void 0 : _api$blockControls$sh.isMenuOpen;
if (options !== null && options !== void 0 && options.closeMenu) {
tr.setMeta(key, {
...currMeta,
closeMenu: true
});
if (currentUserIntent === 'blockMenuOpen') {
var _api$userIntent2;
api === null || api === void 0 ? void 0 : (_api$userIntent2 = api.userIntent) === null || _api$userIntent2 === void 0 ? void 0 : _api$userIntent2.commands.setCurrentUserIntent('default')({
tr
});
}
// When closing the menu, restart the active session timer
if (isMenuCurrentlyOpen && fg('platform_editor_ease_of_use_metrics')) {
var _api$metrics;
api === null || api === void 0 ? void 0 : (_api$metrics = api.metrics) === null || _api$metrics === void 0 ? void 0 : _api$metrics.commands.startActiveSessionTimer()({
tr
});
}
return tr;
}
// Do not open menu on layoutColumn and close opened menu when layoutColumn drag handle is clicked
if (options !== null && options !== void 0 && (_options$anchorName = options.anchorName) !== null && _options$anchorName !== void 0 && _options$anchorName.includes('layoutColumn')) {
if (currentUserIntent === 'blockMenuOpen') {
var _api$userIntent3;
api === null || api === void 0 ? void 0 : (_api$userIntent3 = api.userIntent) === null || _api$userIntent3 === void 0 ? void 0 : _api$userIntent3.commands.setCurrentUserIntent('default')({
tr
});
}
tr.setMeta(key, {
...currMeta,
closeMenu: true
});
// When closing the menu, restart the active session timer
if (isMenuCurrentlyOpen && fg('platform_editor_ease_of_use_metrics')) {
var _api$metrics2;
api === null || api === void 0 ? void 0 : (_api$metrics2 = api.metrics) === null || _api$metrics2 === void 0 ? void 0 : _api$metrics2.commands.startActiveSessionTimer()({
tr
});
}
return tr;
}
let toggleMenuMeta = {
anchorName: options === null || options === void 0 ? void 0 : options.anchorName,
triggerByNode: options === null || options === void 0 ? void 0 : options.triggerByNode
};
const menuTriggerBy = api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : (_api$blockControls2$s = _api$blockControls2.sharedState.currentState()) === null || _api$blockControls2$s === void 0 ? void 0 : _api$blockControls2$s.menuTriggerBy;
if (options !== null && options !== void 0 && options.anchorName) {
const {
moveUp,
moveDown
} = canMoveNodeUpOrDown(tr);
toggleMenuMeta = {
...toggleMenuMeta,
moveUp,
moveDown,
openedViaKeyboard: options === null || options === void 0 ? void 0 : options.openedViaKeyboard
};
}
tr.setMeta(key, {
...currMeta,
toggleMenu: toggleMenuMeta
});
if ((menuTriggerBy === undefined || !!menuTriggerBy && menuTriggerBy === (options === null || options === void 0 ? void 0 : options.anchorName)) && currentUserIntent === 'blockMenuOpen') {
const state = api === null || api === void 0 ? void 0 : api.blockControls.sharedState.currentState();
if (state !== null && state !== void 0 && state.isSelectedViaDragHandle) {
var _api$userIntent4;
api === null || api === void 0 ? void 0 : (_api$userIntent4 = api.userIntent) === null || _api$userIntent4 === void 0 ? void 0 : _api$userIntent4.commands.setCurrentUserIntent('dragHandleSelected')({
tr
});
} else {
var _api$userIntent5;
// Toggled from drag handle
api === null || api === void 0 ? void 0 : (_api$userIntent5 = api.userIntent) === null || _api$userIntent5 === void 0 ? void 0 : _api$userIntent5.commands.setCurrentUserIntent('default')({
tr
});
}
// When closing the menu, restart the active session timer
if (fg('platform_editor_ease_of_use_metrics')) {
var _api$metrics3;
api === null || api === void 0 ? void 0 : (_api$metrics3 = api.metrics) === null || _api$metrics3 === void 0 ? void 0 : _api$metrics3.commands.startActiveSessionTimer()({
tr
});
}
} else if (!isMenuCurrentlyOpen) {
// When opening the menu, pause the active session timer
if (fg('platform_editor_ease_of_use_metrics')) {
var _api$metrics4;
api === null || api === void 0 ? void 0 : (_api$metrics4 = api.metrics) === null || _api$metrics4 === void 0 ? void 0 : _api$metrics4.commands.handleIntentToStartEdit({
shouldStartTimer: false,
shouldPersistActiveSession: true
})({
tr
});
}
}
return tr;
},
setNodeDragged: (getPos, anchorName, nodeType) => ({
tr
}) => {
var _api$userIntent6;
const pos = getPos();
if (pos === undefined) {
return tr;
}
const currMeta = tr.getMeta(key);
tr.setMeta(key, {
...currMeta,
isDragging: true,
activeNode: {
pos,
anchorName,
nodeType
}
});
if (fg('platform_editor_ease_of_use_metrics')) {
var _api$metrics5;
api === null || api === void 0 ? void 0 : (_api$metrics5 = api.metrics) === null || _api$metrics5 === void 0 ? void 0 : _api$metrics5.commands.handleIntentToStartEdit({
shouldStartTimer: false,
shouldPersistActiveSession: true
})({
tr
});
}
api === null || api === void 0 ? void 0 : (_api$userIntent6 = api.userIntent) === null || _api$userIntent6 === void 0 ? void 0 : _api$userIntent6.commands.setCurrentUserIntent('dragging')({
tr
});
return tr;
},
setMultiSelectPositions: (anchor, head) => ({
tr
}) => {
var _api$selection, _$to$nodeBefore, _$from$nodeAfter;
const {
anchor: userAnchor,
head: userHead
} = tr.selection;
let $expandedAnchor, $expandedHead;
if (anchor !== undefined && head !== undefined) {
$expandedAnchor = tr.doc.resolve(anchor);
$expandedHead = tr.doc.resolve(head);
} else {
const expandedSelection = expandSelectionBounds(tr.selection.$anchor, tr.selection.$head);
$expandedAnchor = expandedSelection.$anchor;
$expandedHead = expandedSelection.$head;
}
api === null || api === void 0 ? void 0 : (_api$selection = api.selection) === null || _api$selection === void 0 ? void 0 : _api$selection.commands.setManualSelection($expandedAnchor.pos, $expandedHead.pos)({
tr
});
const $from = $expandedAnchor.min($expandedHead);
const $to = $expandedAnchor.max($expandedHead);
let expandedNormalisedSel;
if ($from.nodeAfter === $to.nodeBefore) {
selectNode(tr, $from.pos, $expandedAnchor.node().type.name, api);
expandedNormalisedSel = tr.selection;
} else if (((_$to$nodeBefore = $to.nodeBefore) === null || _$to$nodeBefore === void 0 ? void 0 : _$to$nodeBefore.type.name) === 'mediaSingle' || ((_$from$nodeAfter = $from.nodeAfter) === null || _$from$nodeAfter === void 0 ? void 0 : _$from$nodeAfter.type.name) === 'mediaSingle') {
expandedNormalisedSel = new TextSelection($expandedAnchor, $expandedHead);
tr.setSelection(expandedNormalisedSel);
} else {
// this is to normalise the selection's boundaries to inline positions, preventing it from collapsing
expandedNormalisedSel = TextSelection.between($expandedAnchor, $expandedHead);
tr.setSelection(expandedNormalisedSel);
}
const multiSelectDnD = {
anchor: $expandedAnchor.pos,
head: $expandedHead.pos,
textAnchor: expandedNormalisedSel.anchor,
textHead: expandedNormalisedSel.head,
userAnchor: userAnchor,
userHead: userHead
};
const currMeta = tr.getMeta(key);
tr.setMeta(key, {
...currMeta,
multiSelectDnD
});
return tr;
},
setSelectedViaDragHandle: isSelectedViaDragHandle => ({
tr
}) => {
const currMeta = tr.getMeta(key);
return tr.setMeta(key, {
...currMeta,
isSelectedViaDragHandle
});
},
mapPreservedSelection: mapping => mapPreservedSelection(mapping),
moveNodeWithBlockMenu: direction => moveNodeWithBlockMenu(api, direction),
handleKeyDownWithPreservedSelection: handleKeyDownWithPreservedSelection(api),
startPreservingSelection: () => startPreservingSelection,
stopPreservingSelection: () => stopPreservingSelection
},
getSharedState(editorState) {
var _key$getState$isMenuO, _key$getState, _key$getState$menuTri, _key$getState2, _key$getState$menuTri2, _key$getState3, _key$getState$blockMe, _key$getState4, _key$getState$activeN, _key$getState5, _key$getState$activeD, _key$getState6, _key$getState$isDragg, _key$getState7, _key$getState$isPMDra, _key$getState8, _key$getState$multiSe, _key$getState9, _key$getState$isShift, _key$getState0, _key$getState$lastDra, _key$getState1, _interactionTrackingP, _key$getState$isSelec, _key$getState10;
if (!editorState) {
return undefined;
}
const sharedState = {
isMenuOpen: (_key$getState$isMenuO = (_key$getState = key.getState(editorState)) === null || _key$getState === void 0 ? void 0 : _key$getState.isMenuOpen) !== null && _key$getState$isMenuO !== void 0 ? _key$getState$isMenuO : false,
menuTriggerBy: (_key$getState$menuTri = (_key$getState2 = key.getState(editorState)) === null || _key$getState2 === void 0 ? void 0 : _key$getState2.menuTriggerBy) !== null && _key$getState$menuTri !== void 0 ? _key$getState$menuTri : undefined,
menuTriggerByNode: (_key$getState$menuTri2 = (_key$getState3 = key.getState(editorState)) === null || _key$getState3 === void 0 ? void 0 : _key$getState3.menuTriggerByNode) !== null && _key$getState$menuTri2 !== void 0 ? _key$getState$menuTri2 : undefined,
blockMenuOptions: (_key$getState$blockMe = (_key$getState4 = key.getState(editorState)) === null || _key$getState4 === void 0 ? void 0 : _key$getState4.blockMenuOptions) !== null && _key$getState$blockMe !== void 0 ? _key$getState$blockMe : undefined,
activeNode: (_key$getState$activeN = (_key$getState5 = key.getState(editorState)) === null || _key$getState5 === void 0 ? void 0 : _key$getState5.activeNode) !== null && _key$getState$activeN !== void 0 ? _key$getState$activeN : undefined,
activeDropTargetNode: (_key$getState$activeD = (_key$getState6 = key.getState(editorState)) === null || _key$getState6 === void 0 ? void 0 : _key$getState6.activeDropTargetNode) !== null && _key$getState$activeD !== void 0 ? _key$getState$activeD : undefined,
isDragging: (_key$getState$isDragg = (_key$getState7 = key.getState(editorState)) === null || _key$getState7 === void 0 ? void 0 : _key$getState7.isDragging) !== null && _key$getState$isDragg !== void 0 ? _key$getState$isDragg : false,
isPMDragging: (_key$getState$isPMDra = (_key$getState8 = key.getState(editorState)) === null || _key$getState8 === void 0 ? void 0 : _key$getState8.isPMDragging) !== null && _key$getState$isPMDra !== void 0 ? _key$getState$isPMDra : false,
multiSelectDnD: (_key$getState$multiSe = (_key$getState9 = key.getState(editorState)) === null || _key$getState9 === void 0 ? void 0 : _key$getState9.multiSelectDnD) !== null && _key$getState$multiSe !== void 0 ? _key$getState$multiSe : undefined,
isShiftDown: (_key$getState$isShift = (_key$getState0 = key.getState(editorState)) === null || _key$getState0 === void 0 ? void 0 : _key$getState0.isShiftDown) !== null && _key$getState$isShift !== void 0 ? _key$getState$isShift : undefined,
lastDragCancelled: (_key$getState$lastDra = (_key$getState1 = key.getState(editorState)) === null || _key$getState1 === void 0 ? void 0 : _key$getState1.lastDragCancelled) !== null && _key$getState$lastDra !== void 0 ? _key$getState$lastDra : false,
isEditing: (_interactionTrackingP = interactionTrackingPluginKey.getState(editorState)) === null || _interactionTrackingP === void 0 ? void 0 : _interactionTrackingP.isEditing,
isSelectedViaDragHandle: (_key$getState$isSelec = (_key$getState10 = key.getState(editorState)) === null || _key$getState10 === void 0 ? void 0 : _key$getState10.isSelectedViaDragHandle) !== null && _key$getState$isSelec !== void 0 ? _key$getState$isSelec : false
};
if (editorExperiment('platform_editor_controls', 'variant1')) {
var _interactionTrackingP2, _interactionTrackingP3, _interactionTrackingP4;
sharedState.isMouseOut = (_interactionTrackingP2 = (_interactionTrackingP3 = interactionTrackingPluginKey.getState(editorState)) === null || _interactionTrackingP3 === void 0 ? void 0 : _interactionTrackingP3.isMouseOut) !== null && _interactionTrackingP2 !== void 0 ? _interactionTrackingP2 : false;
// rightSideControlsEnabled is the single source of truth (confluence_remix_button_right_side_block_fg from preset)
sharedState.rightSideControlsEnabled = rightSideControlsEnabled;
sharedState.hoverSide = rightSideControlsEnabled ? (_interactionTrackingP4 = interactionTrackingPluginKey.getState(editorState)) === null || _interactionTrackingP4 === void 0 ? void 0 : _interactionTrackingP4.hoverSide : undefined;
}
if (editorExperiment('platform_editor_block_menu', true)) {
var _selectionPreservatio;
sharedState.preservedSelection = (_selectionPreservatio = selectionPreservationPluginKey.getState(editorState)) === null || _selectionPreservatio === void 0 ? void 0 : _selectionPreservatio.preservedSelection;
}
return sharedState;
},
contentComponent() {
return /*#__PURE__*/React.createElement(GlobalStylesWrapper, {
api: api
});
}
};
};