remirror/remirror

View on GitHub
packages/remirror__extension-list/src/bullet-list-extension.ts

Summary

Maintainability
A
0 mins
Test Coverage
F
37%
import {
  ApplySchemaAttributes,
  assertGet,
  command,
  CommandFunction,
  extension,
  ExtensionPriority,
  ExtensionTag,
  keyBinding,
  KeyBindingProps,
  NamedShortcut,
  NodeExtension,
  NodeExtensionSpec,
  NodeSpecOverride,
  NodeViewMethod,
  ProsemirrorNode,
  Static,
} from '@remirror/core';
import { ExtensionListMessages as Messages } from '@remirror/messages';
import { InputRule, wrappingInputRule } from '@remirror/pm/inputrules';
import { NodeSelection } from '@remirror/pm/state';
import { ExtensionListTheme } from '@remirror/theme';

import { toggleList, wrapSelectedItems } from './list-commands';
import { ListItemExtension } from './list-item-extension';

/**
 * Create the node for a bullet list.
 */
@extension<BulletListOptions>({
  defaultOptions: { enableSpine: false },
  staticKeys: ['enableSpine'],
})
export class BulletListExtension extends NodeExtension<BulletListOptions> {
  get name() {
    return 'bulletList' as const;
  }

  createTags() {
    return [ExtensionTag.Block, ExtensionTag.ListContainerNode];
  }

  createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
    return {
      content: 'listItem+',
      ...override,
      attrs: extra.defaults(),
      parseDOM: [{ tag: 'ul', getAttrs: extra.parse }, ...(override.parseDOM ?? [])],
      toDOM: (node) => ['ul', extra.dom(node), 0],
    };
  }

  createNodeViews(): NodeViewMethod | Record<string, never> {
    if (!this.options.enableSpine) {
      return {};
    }

    return (_, view, getPos) => {
      const dom = document.createElement('div');
      dom.style.position = 'relative';

      const pos = (getPos as () => number)();
      const $pos = view.state.doc.resolve(pos + 1);

      const parentListItemNode: ProsemirrorNode | undefined = $pos.node($pos.depth - 1);

      const isFirstLevel = parentListItemNode?.type?.name !== 'listItem';

      if (!isFirstLevel) {
        const spine = document.createElement('div');
        spine.contentEditable = 'false';
        spine.classList.add(ExtensionListTheme.LIST_SPINE);

        spine.addEventListener('click', (event) => {
          const pos = (getPos as () => number)();
          const $pos = view.state.doc.resolve(pos + 1);
          const parentListItemPos: number = $pos.start($pos.depth - 1);
          const selection = NodeSelection.create(view.state.doc, parentListItemPos - 1);
          view.dispatch(view.state.tr.setSelection(selection));
          this.store.commands.toggleListItemClosed();

          event.preventDefault();
          event.stopPropagation();
        });
        dom.append(spine);
      }

      const contentDOM = document.createElement('ul');
      contentDOM.classList.add(ExtensionListTheme.UL_LIST_CONTENT);
      dom.append(contentDOM);

      return {
        dom,
        contentDOM,
      };
    };
  }

  createExtensions() {
    return [
      new ListItemExtension({
        priority: ExtensionPriority.Low,
        enableCollapsible: this.options.enableSpine,
      }),
    ];
  }

  /**
   * Toggle the bullet list for the current selection.
   */
  @command({ icon: 'listUnordered', label: ({ t }) => t(Messages.BULLET_LIST_LABEL) })
  toggleBulletList(): CommandFunction {
    return toggleList(this.type, assertGet(this.store.schema.nodes, 'listItem'));
  }

  @keyBinding({ shortcut: NamedShortcut.BulletList, command: 'toggleBulletList' })
  listShortcut(props: KeyBindingProps): boolean {
    return this.toggleBulletList()(props);
  }

  createInputRules(): InputRule[] {
    const regexp = /^\s*([*+-])\s$/;

    return [
      wrappingInputRule(regexp, this.type),

      new InputRule(regexp, (state, _match, start, end) => {
        const tr = state.tr;
        tr.deleteRange(start, end);
        const canUpdate = wrapSelectedItems({
          listType: this.type,
          itemType: assertGet(this.store.schema.nodes, 'listItem'),
          tr,
        });

        if (!canUpdate) {
          return null;
        }

        return tr;
      }),
    ];
  }
}

export interface BulletListOptions {
  /**
   * Set this to true to add a spine.
   */
  enableSpine?: Static<boolean>;
}

declare global {
  namespace Remirror {
    interface AllExtensions {
      bulletList: BulletListExtension;
    }
  }
}