UNPKG

@atlaskit/editor-plugin-code-block

Version:

Code block plugin for @atlaskit/editor-core

155 lines (152 loc) 6.98 kB
/* eslint-disable @atlaskit/platform/ensure-feature-flag-prefix */ import { browser } 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 { fg } from '@atlaskit/platform-feature-flags'; 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, getAllCodeBlockNodesInDoc } from './utils'; export const createPlugin = ({ useLongPressSelection = false, getIntl, allowCompositionInputOverride = false, api }) => { const handleDOMEvents = {}; // 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; 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 = generateInitialDecorations(state); return { pos: node ? node.pos : null, contentCopied: false, isNodeSelected: false, shouldIgnoreFollowingMutations: false, decorations: DecorationSet.create(state.doc, initialDecorations) }; }, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/max-params 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 = fg('editor_code_wrapping_perf_improvement_ed-25141') ? getAllChangedCodeBlocksInTransaction(tr) : getAllCodeBlockNodesInDoc(newState); 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 } }); };