@atlaskit/editor-plugin-code-block
Version: 
Code block plugin for @atlaskit/editor-core
155 lines (152 loc) • 6.98 kB
JavaScript
/* 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
    }
  });
};