remirror/remirror

View on GitHub
packages/remirror__extension-columns/src/columns-extension.ts

Summary

Maintainability
A
0 mins
Test Coverage
F
39%
import {
  ApplySchemaAttributes,
  command,
  CommandFunction,
  extension,
  ExtensionTag,
  IdentifierSchemaAttributes,
  isElementDomNode,
  joinStyles,
  LiteralUnion,
  NodeExtension,
  NodeExtensionSpec,
  NodeSpecOverride,
  omitExtraAttributes,
  PrimitiveSelection,
  ProsemirrorAttributes,
  SchemaAttributesObject,
  Static,
} from '@remirror/core';
import { ExtensionColumnsMessages as Messages } from '@remirror/messages';

export const toggleColumnsOptions: Remirror.CommandDecoratorOptions = {
  icon: ({ attrs }) => ({
    name: 'layoutColumnLine',
    sup: attrs?.count as string,
  }),
  label: ({ t, attrs }) =>
    t({
      ...Messages.LABEL,
      values: {
        count: attrs?.count,
      },
    }),
  description: ({ t, attrs }) =>
    t({
      ...Messages.DESCRIPTION,
      values: {
        count: attrs?.count,
      },
    }),
};

export const DEFAULT_COLUMN_ATTRIBUTES: Required<BaseColumnAttributes> = {
  count: 2,
  fill: 'auto',
  gap: 'inherit',
  ruleColor: 'inherit',
  ruleStyle: 'none',
  ruleWidth: 'inherit',
  width: 'inherit',
};
const COLUMN_DATA_ATTRIBUTE = 'data-column-type';

export interface ColumnsOptions {
  /**
   * The default columns to use for created columns.
   *
   * @defaultValue `DEFAULT_COLUMN_ATTRIBUTES`
   */
  defaults?: Static<Required<BaseColumnAttributes>>;
}

export interface BaseColumnAttributes {
  /**
   * Specifies the number of columns an element should be divided into.
   *
   * @defaultValue 2
   */
  count?: number;

  /**
   * Specifies how to fill columns.
   *
   * @defaultValue 'auto'
   */
  fill?: 'balance' | 'auto';

  /**
   * Specifies the gap between the columns.
   *
   * @defaultValue 'inherit'
   */
  gap?: string;

  /**
   * Specifies the color of the rule between columns.
   *
   * @defaultValue 'transparent'
   */
  ruleColor?: string;

  /**
   * Specifies the style of the rule between columns.
   *
   * @defaultValue 'none'
   */
  ruleStyle?:
    | 'none'
    | 'hidden'
    | 'dotted'
    | 'dashed'
    | 'solid'
    | 'double'
    | 'groove'
    | 'ridge'
    | 'inset'
    | 'outset';

  /**
   * Specifies the width of the rule between columns.
   *
   * @defaultValue 'inherit'
   */
  ruleWidth?: LiteralUnion<'medium' | 'thin' | 'thick', string>;

  /**
   * Specifies a suggested, optimal width for the columns.
   *
   * @defaultValue 'inherit'
   */
  width?: string;
}

export type ColumnAttributes = ProsemirrorAttributes<BaseColumnAttributes>;

/**
 * Add column support to the nodes in your editor.
 */
@extension<ColumnsOptions>({
  defaultOptions: {
    defaults: DEFAULT_COLUMN_ATTRIBUTES,
  },
  staticKeys: ['defaults'],
})
export class ColumnsExtension extends NodeExtension<ColumnsOptions> {
  get name() {
    return 'columns' as const;
  }

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

  createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
    return {
      ...override,
      content: 'block+',
      attrs: {
        ...extra.defaults(),
        count: {
          default: this.options.defaults.count,
        },
        fill: {
          default: this.options.defaults.fill,
        },
        gap: {
          default: this.options.defaults.gap,
        },
        ruleColor: {
          default: this.options.defaults.ruleColor,
        },
        ruleStyle: {
          default: this.options.defaults.ruleStyle,
        },
        ruleWidth: {
          default: this.options.defaults.ruleWidth,
        },
        width: {
          default: this.options.defaults.width,
        },
      },
      parseDOM: [
        {
          tag: `div[${COLUMN_DATA_ATTRIBUTE}]`,
          getAttrs: (node) => {
            if (!isElementDomNode(node)) {
              return false;
            }

            const {
              columnCount,
              columnFill,
              columnGap,
              columnRuleColor,
              columnRuleStyle,
              columnRuleWidth,
              columnWidth,
            } = node.style;

            return {
              ...extra.parse(node),
              count: columnCount,
              fill: columnFill,
              gap: columnGap,
              ruleColor: columnRuleColor,
              ruleStyle: columnRuleStyle,
              ruleWidth: columnRuleWidth,
              width: columnWidth,
            };
          },
        },
        ...(override.parseDOM ?? []),
      ],
      toDOM: (node) => {
        const { count, fill, gap, ruleColor, ruleStyle, ruleWidth, width, ...other } =
          omitExtraAttributes<Required<ColumnAttributes>>(node.attrs, extra);
        const { style: currentStyle = '', ...rest } = extra.dom(node);
        const style = joinStyles(
          {
            columnCount: count,
            columnFill: fill,
            columnGap: gap,
            columnRuleColor: ruleColor,
            columnRuleStyle: ruleStyle,
            columnRuleWidth: ruleWidth,
            columnWidth: width,
          },
          currentStyle,
        );
        const attributes = {
          ...rest,
          ...other,
          style,
          [COLUMN_DATA_ATTRIBUTE]: 'true',
        };

        return ['div', attributes, 0];
      },
    };
  }

  /**
   * Add a column span attribute to all block nodes within the editor.
   */
  createSchemaAttributes(): IdentifierSchemaAttributes[] {
    const columnSpan: SchemaAttributesObject = {
      default: null,
      parseDOM: (node) => node.getAttribute('column-span') ?? 'none',
      toDOM: (attrs) =>
        attrs.columnSpan ? ['column-span', attrs.columnSpan === 'all' ? 'all' : 'none'] : null,
    };

    return [
      {
        identifiers: {
          tags: [ExtensionTag.Block],
          type: 'node',
          excludeNames: ['columns'],
        },
        attributes: {
          columnSpan,
        },
      },
    ];
  }

  /**
   * Toggle a column wrap around the content.
   */
  @command(toggleColumnsOptions)
  toggleColumns(attrs: ColumnAttributes = {}, options: ToggleColumnsOptions = {}): CommandFunction {
    return this.store.commands.toggleWrappingNode.original(this.type, attrs, options.selection);
  }
}

interface ToggleColumnsOptions {
  selection?: PrimitiveSelection;
}

declare global {
  namespace Remirror {
    interface Attributes {
      /**
       * The number of columns that a node should span across. This only comes
       * into effect if the block node is within a column node.
       */
      columnSpan?: 'none' | 'all';
    }

    interface AllExtensions {
      columns: ColumnsExtension;
    }
  }
}