remirror/remirror

View on GitHub
packages/remirror__core/src/builtins/suggest-extension.ts

Summary

Maintainability
A
0 mins
Test Coverage
C
78%
import { includes, isArray } from '@remirror/core-helpers';
import type { CustomHandler, EditorState, ProsemirrorPlugin } from '@remirror/core-types';
import {
  addSuggester,
  getSuggestPluginState,
  removeSuggester,
  suggest,
  Suggester,
  SuggestState,
} from '@remirror/pm/suggest';

import { extension, Helper, PlainExtension } from '../extension';
import type { AddCustomHandler } from '../extension/base-class';
import { helper } from './builtin-decorators';

export interface SuggestOptions {
  /**
   * The custom handler which enables adding `suggesters`.
   */
  suggester: CustomHandler<Suggester>;
}

/**
 * This extension allows others extension to add the `createSuggesters` method
 * for adding the prosemirror-suggest functionality to your editor.
 *
 * @remarks
 *
 * This is an example of adding custom functionality to an extension via the
 * `ExtensionParameterMethods`.
 *
 * @category Builtin Extension
 */
@extension<SuggestOptions>({ customHandlerKeys: ['suggester'] })
export class SuggestExtension extends PlainExtension<SuggestOptions> {
  get name() {
    return 'suggest' as const;
  }

  /**
   * Create the `addSuggester` method and `removeSuggester` methods to the
   * extension store.
   *
   * This can be used by extensions to conditionally add suggestion support.
   */
  onCreate(): void {
    this.store.setExtensionStore('addSuggester', (suggester) =>
      addSuggester(this.store.getState(), suggester),
    );

    this.store.setExtensionStore('removeSuggester', (suggester) =>
      removeSuggester(this.store.getState(), suggester),
    );
  }

  /**
   * Add the `prosemirror-suggest` plugin to the editor.
   */
  createExternalPlugins(): ProsemirrorPlugin[] {
    const suggesters: Suggester[] = [];

    for (const extension of this.store.extensions) {
      if (this.store.managerSettings.exclude?.suggesters) {
        // Exit the loop early when the manager is set to ignore suggesters.
        break;
      }

      if (
        // Method doesn't exist
        !extension.createSuggesters ||
        // Extension settings exclude it from running
        extension.options.exclude?.suggesters
      ) {
        continue;
      }

      const suggester = extension.createSuggesters();
      const suggesterList = isArray(suggester) ? suggester : [suggester];
      suggesters.push(...suggesterList);
    }

    return [suggest(...suggesters)];
  }

  /**
   * Allow additional `Suggesters` to be added to the editor. This can be used
   * by `React` to create hooks.
   */
  onAddCustomHandler: AddCustomHandler<SuggestOptions> = ({ suggester }) => {
    if (!suggester || this.store.managerSettings.exclude?.suggesters) {
      return;
    }

    // Update the suggesters with the provided suggester. Returns the cleanup
    // method.
    return addSuggester(this.store.getState(), suggester);
  };

  /**
   * Get the suggest plugin state.
   *
   * This may be removed at a later time.
   *
   * @experimental
   */
  @helper()
  getSuggestState(state?: EditorState): Helper<SuggestState> {
    return getSuggestPluginState(state ?? this.store.getState());
  }

  /**
   * Get some helpful methods from the SuggestPluginState.
   */
  @helper()
  getSuggestMethods(): Helper<
    Pick<
      SuggestState,
      | 'addIgnored'
      | 'clearIgnored'
      | 'removeIgnored'
      | 'ignoreNextExit'
      | 'setMarkRemoved'
      | 'findMatchAtPosition'
      | 'findNextTextSelection'
      | 'setLastChangeFromAppend'
    >
  > {
    const {
      addIgnored,
      clearIgnored,
      removeIgnored,
      ignoreNextExit,
      setMarkRemoved,
      findMatchAtPosition,
      findNextTextSelection,
      setLastChangeFromAppend,
    } = this.getSuggestState();

    return {
      addIgnored,
      clearIgnored,
      removeIgnored,
      ignoreNextExit,
      setMarkRemoved,
      findMatchAtPosition,
      findNextTextSelection,
      setLastChangeFromAppend,
    };
  }

  /**
   * Check to see whether the provided name is the currently active
   * suggester.
   *
   * @param name - the name of the suggester to include
   */
  @helper()
  isSuggesterActive(name: string | string[]): Helper<boolean> {
    return includes(isArray(name) ? name : [name], this.getSuggestState().match?.suggester.name);
  }
}

declare global {
  namespace Remirror {
    interface ExcludeOptions {
      /**
       * Whether to exclude the suggesters plugin configuration for the
       * extension.
       *
       * @defaultValue undefined
       */
      suggesters?: boolean;
    }

    interface BaseExtension {
      /**
       * Create suggesters which respond to an activation `char` or regex
       * pattern within the editor instance. The onChange handler provided is
       * called with the data around the matching text.
       *
       * @remarks
       *
       * Suggesters are a  powerful way of building up the editors
       * functionality. They can support `@` mentions, `#` tagging, `/` special
       * command keys which trigger action menus and much more.
       */
      createSuggesters?(): Suggester[] | Suggester;
    }

    interface AllExtensions {
      suggest: SuggestExtension;
    }

    interface ExtensionStore {
      /**
       * Add a suggester.
       */
      addSuggester(suggester: Suggester): void;

      /**
       * Remove a suggester.
       */
      removeSuggester(suggester: Suggester | string): void;
    }

    interface AllExtensions {
      suggest: SuggestExtension;
    }
  }
}