@atlaskit/editor-plugin-selection-marker
Version:
Selection marker plugin for @atlaskit/editor-core.
136 lines (135 loc) • 8.03 kB
JavaScript
import React, { useEffect, useRef } from 'react';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
import { useSharedPluginStateSelector } from '@atlaskit/editor-common/use-shared-plugin-state-selector';
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { createPlugin, dispatchShouldHideDecorations, key } from './pm-plugins/main';
import { GlobalStylesWrapper } from './ui/global-styles';
export const selectionMarkerPlugin = ({
config,
api
}) => {
return {
name: 'selectionMarker',
pmPlugins() {
return [{
name: 'selectionMarkerPmPlugin',
plugin: () => createPlugin(api)
}];
},
getSharedState(editorState) {
var _key$getState$forceHi, _key$getState, _key$getState2;
if (!editorState) {
return undefined;
}
return {
isForcedHidden: (_key$getState$forceHi = (_key$getState = key.getState(editorState)) === null || _key$getState === void 0 ? void 0 : _key$getState.forceHide) !== null && _key$getState$forceHi !== void 0 ? _key$getState$forceHi : false,
isMarkerActive: !((_key$getState2 = key.getState(editorState)) !== null && _key$getState2 !== void 0 && _key$getState2.shouldHideDecorations)
};
},
actions: {
// For now this is a very simple locking mechanism that only allows one
// plugin to hide / release at a time.
hideDecoration: () => {
var _api$selectionMarker, _api$selectionMarker$, _api$core;
if (api !== null && api !== void 0 && (_api$selectionMarker = api.selectionMarker) !== null && _api$selectionMarker !== void 0 && (_api$selectionMarker$ = _api$selectionMarker.sharedState.currentState()) !== null && _api$selectionMarker$ !== void 0 && _api$selectionMarker$.isForcedHidden) {
return undefined;
}
const success = api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
tr
}) => tr.setMeta(key, {
forceHide: true
}));
if (!success) {
return undefined;
}
return cleanupHiddenDecoration(api);
},
queueHideDecoration: setCleanup => {
const result = api === null || api === void 0 ? void 0 : api.selectionMarker.actions.hideDecoration();
if (result === undefined) {
var _api$selectionMarker2;
const cleanup = api === null || api === void 0 ? void 0 : (_api$selectionMarker2 = api.selectionMarker) === null || _api$selectionMarker2 === void 0 ? void 0 : _api$selectionMarker2.sharedState.onChange(({
nextSharedState
}) => {
if ((nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.isForcedHidden) === false && (nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.isMarkerActive) === false) {
const result = api === null || api === void 0 ? void 0 : api.selectionMarker.actions.hideDecoration();
setCleanup(result);
cleanup === null || cleanup === void 0 ? void 0 : cleanup();
}
});
return cleanup;
}
setCleanup(result);
return () => {};
}
},
usePluginHook({
editorView
}) {
const editorHasNotBeenFocused = useRef(true);
useEffect(() => {
// relatch when editorView changes (pretty good signal for reinit)
editorHasNotBeenFocused.current = true;
}, [editorView]);
const {
hasFocus,
isOpen,
editorDisabled,
showToolbar,
hasDangerDecorations,
currentUserIntent
} = useSharedPluginStateWithSelector(api, ['focus', 'typeAhead', 'editorDisabled', 'toolbar', 'decorations', 'userIntent'], states => {
var _states$focusState, _states$typeAheadStat, _states$editorDisable, _states$toolbarState, _states$decorationsSt, _states$userIntentSta;
return {
hasFocus: (_states$focusState = states.focusState) === null || _states$focusState === void 0 ? void 0 : _states$focusState.hasFocus,
isOpen: (_states$typeAheadStat = states.typeAheadState) === null || _states$typeAheadStat === void 0 ? void 0 : _states$typeAheadStat.isOpen,
editorDisabled: (_states$editorDisable = states.editorDisabledState) === null || _states$editorDisable === void 0 ? void 0 : _states$editorDisable.editorDisabled,
showToolbar: (_states$toolbarState = states.toolbarState) === null || _states$toolbarState === void 0 ? void 0 : _states$toolbarState.shouldShowToolbar,
hasDangerDecorations: expValEqualsNoExposure('platform_editor_block_menu', 'isEnabled', true) ? (_states$decorationsSt = states.decorationsState) === null || _states$decorationsSt === void 0 ? void 0 : _states$decorationsSt.hasDangerDecorations : undefined,
currentUserIntent: (_states$userIntentSta = states.userIntentState) === null || _states$userIntentSta === void 0 ? void 0 : _states$userIntentSta.currentUserIntent
};
});
const isForcedHidden = useSharedPluginStateSelector(api, 'selectionMarker.isForcedHidden');
useEffect(() => {
// On editor init we should use this latch to keep the marker hidden until
// editor has received focus. This means editor will be initially hidden until
// the first focus occurs, and after first focus the normal above rules will
// apply
if (hasFocus === true) {
editorHasNotBeenFocused.current = false;
}
const isBlockMenuOpen = currentUserIntent === 'blockMenuOpen' && editorExperiment('platform_editor_block_menu', true);
/**
* There are a number of conditions we should not show the marker,
* - Editor has not been focused: to keep the marker hidden until first focus if config is set
* - Focus: to ensure it doesn't interrupt the normal cursor
* - Typeahead Open: To ensure it doesn't show when we're typing in the typeahead
* - Disabled: So that it behaves similar to the renderer in live pages/disabled
* - Via the API: If another plugin has requested it to be hidden (force hidden).
* - If danger styles is shown in decorationsPlugin, then we don't need to show the selection marker
*/
const shouldHide = (config === null || config === void 0 ? void 0 : config.hideCursorOnInit) && editorHasNotBeenFocused.current || hasFocus || (isOpen !== null && isOpen !== void 0 ? isOpen : false) || isForcedHidden || (editorDisabled !== null && editorDisabled !== void 0 ? editorDisabled : false) || (showToolbar !== null && showToolbar !== void 0 ? showToolbar : false) || !!hasDangerDecorations && editorExperiment('platform_editor_block_menu', true) || isBlockMenuOpen;
requestAnimationFrame(() => dispatchShouldHideDecorations(editorView, shouldHide));
}, [editorView, hasFocus, isOpen, isForcedHidden, editorDisabled, showToolbar, hasDangerDecorations, currentUserIntent]);
},
contentComponent() {
return /*#__PURE__*/React.createElement(GlobalStylesWrapper, null);
}
};
};
function cleanupHiddenDecoration(api) {
let hasRun = false;
return () => {
var _api$selectionMarker3, _api$selectionMarker4;
if (!hasRun && api !== null && api !== void 0 && (_api$selectionMarker3 = api.selectionMarker) !== null && _api$selectionMarker3 !== void 0 && (_api$selectionMarker4 = _api$selectionMarker3.sharedState.currentState()) !== null && _api$selectionMarker4 !== void 0 && _api$selectionMarker4.isForcedHidden) {
var _api$core2;
hasRun = true;
return api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
tr
}) => tr.setMeta(key, {
forceHide: false
}));
}
};
}