@atlaskit/editor-plugin-code-block
Version:
Code block plugin for @atlaskit/editor-core
164 lines (162 loc) • 7.76 kB
JavaScript
import { getBrowserInfo } from '@atlaskit/editor-common/browser';
import { updateCodeBlockWrappedStateNodeKeys } from '@atlaskit/editor-common/code-block';
import { blockTypeMessages } from '@atlaskit/editor-common/messages';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { createSelectionClickHandler } from '@atlaskit/editor-common/selection';
import { findCodeBlock } from '@atlaskit/editor-common/transforms';
import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { ignoreFollowingMutations, resetShouldIgnoreFollowingMutations } from '../editor-commands';
import { codeBlockNodeView } from '../nodeviews/code-block';
import { codeBlockClassNames } from '../ui/class-names';
import { ACTIONS } from './actions';
import { generateInitialDecorations, updateCodeBlockDecorations, updateDecorationSetWithWordWrappedDecorator } from './decorators';
import { pluginKey } from './plugin-key';
import { getAllChangedCodeBlocksInTransaction } from './utils';
export const createPlugin = ({
useLongPressSelection = false,
getIntl,
allowCompositionInputOverride = false,
api
}) => {
const handleDOMEvents = {
click: () => {
var _api$core, _api$interaction;
// Set hasHadInteraction to true on any click of code blocks, as clicks
// on code blocks to not propagate to editor-level click handlers
api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(api === null || api === void 0 ? void 0 : (_api$interaction = api.interaction) === null || _api$interaction === void 0 ? void 0 : _api$interaction.commands.handleInteraction);
}
};
// ME-1599: Composition on mobile was causing the DOM observer to mutate the code block
// incorrecly and lose content when pressing enter in the middle of a code block line.
if (allowCompositionInputOverride) {
handleDOMEvents.beforeinput = (view, event) => {
const keyEvent = event;
const eventInputType = keyEvent.inputType;
const eventText = keyEvent.data;
const browser = getBrowserInfo();
if (browser.ios && event.composed &&
// insertParagraph will be the input type when the enter key is pressed.
eventInputType === 'insertParagraph' && findCodeBlock(view.state, view.state.selection)) {
event.preventDefault();
return true;
} else if (browser.android && event.composed && eventInputType === 'insertCompositionText' && eventText[(eventText === null || eventText === void 0 ? void 0 : eventText.length) - 1] === '\n' && findCodeBlock(view.state, view.state.selection)) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resultingText = event.target.outerText + '\n';
if (resultingText.endsWith(eventText)) {
// End of paragraph
setTimeout(() => {
view.someProp('handleKeyDown', f => f(view, new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
key: 'Enter',
code: 'Enter'
})));
}, 0);
} else {
// Middle of paragraph, end of line
ignoreFollowingMutations(view.state, view.dispatch);
}
return true;
}
if (browser.android) {
resetShouldIgnoreFollowingMutations(view.state, view.dispatch);
}
return false;
};
}
return new SafePlugin({
state: {
init(_, state) {
const node = findCodeBlock(state, state.selection);
const initialDecorations =
// Disabled when using advanced code block advanced since it interfers with styling
// Long term we will deprecate the code block plugin and move all the related logic to
// the advanced plugin
// @ts-expect-error Code block advanced cannot depend on code block
(api === null || api === void 0 ? void 0 : api.codeBlockAdvanced) !== undefined && expValEquals('platform_editor_code_block_fold_gutter', 'isEnabled', true) ? [] : generateInitialDecorations(state);
return {
pos: node ? node.pos : null,
contentCopied: false,
isNodeSelected: false,
shouldIgnoreFollowingMutations: false,
decorations: DecorationSet.create(state.doc, initialDecorations)
};
},
apply(tr, pluginState, _oldState, newState) {
const meta = tr.getMeta(pluginKey);
if ((meta === null || meta === void 0 ? void 0 : meta.type) === ACTIONS.SET_IS_WRAPPED) {
const node = findCodeBlock(newState, tr.selection);
return {
...pluginState,
decorations: updateDecorationSetWithWordWrappedDecorator(pluginState.decorations, tr, node)
};
}
if (tr.docChanged) {
const node = findCodeBlock(newState, tr.selection);
// Updates mapping position of all existing decorations to new positions
// specifically used for updating word wrap node decorators (does not cover drag & drop, validateWordWrappedDecorators does).
let updatedDecorationSet = pluginState.decorations.map(tr.mapping, tr.doc);
const codeBlockNodes = getAllChangedCodeBlocksInTransaction(tr);
if (codeBlockNodes) {
updateCodeBlockWrappedStateNodeKeys(codeBlockNodes, _oldState);
// Disabled when using advanced code block for performance reasons
// @ts-expect-error Code block advanced cannot depend on code block
if ((api === null || api === void 0 ? void 0 : api.codeBlockAdvanced) === undefined) {
updatedDecorationSet = updateCodeBlockDecorations(tr, codeBlockNodes, updatedDecorationSet);
}
}
const newPluginState = {
...pluginState,
pos: node ? node.pos : null,
isNodeSelected: tr.selection instanceof NodeSelection,
decorations: updatedDecorationSet
};
return newPluginState;
}
if (tr.selectionSet) {
const node = findCodeBlock(newState, tr.selection);
const newPluginState = {
...pluginState,
pos: node ? node.pos : null,
isNodeSelected: tr.selection instanceof NodeSelection
};
return newPluginState;
}
if ((meta === null || meta === void 0 ? void 0 : meta.type) === ACTIONS.SET_COPIED_TO_CLIPBOARD) {
return {
...pluginState,
contentCopied: meta.data
};
} else if ((meta === null || meta === void 0 ? void 0 : meta.type) === ACTIONS.SET_SHOULD_IGNORE_FOLLOWING_MUTATIONS) {
return {
...pluginState,
shouldIgnoreFollowingMutations: meta.data
};
}
return pluginState;
}
},
key: pluginKey,
props: {
decorations(state) {
return pluginKey.getState(state).decorations || DecorationSet.empty;
},
nodeViews: {
codeBlock: (node, view, getPos) => {
const {
formatMessage
} = getIntl();
const formattedAriaLabel = formatMessage(blockTypeMessages.codeblock);
return codeBlockNodeView(node, view, getPos, formattedAriaLabel, api);
}
},
handleClickOn: createSelectionClickHandler(['codeBlock'], target => !!(target.classList.contains(codeBlockClassNames.lineNumberWidget) || target.classList.contains(codeBlockClassNames.gutter)), {
useLongPressSelection
}),
handleDOMEvents
}
});
};