editor-js / list

Advanced List tool for the Editor.js.
MIT License
48 stars 47 forks source link

Max level depth implementation #80

Closed ccppoo closed 1 week ago

ccppoo commented 3 months ago
import { BlockToolConstructorOptions } from '@editorjs/editorjs/types/tools';
import type { NestedListParams } from '@editorjs/nested-list';
import NestedList from '@editorjs/nested-list';

// copied from original code == start ==
type ListDataStyle = 'ordered' | 'unordered';

interface ListItem {
  /**
   * list item text content
   */
  content: string;
  /**
   * sublist items
   */
  items: ListItem[];
}
interface ListData {
  /**
   * list type 'ordered' or 'unordered'
   */
  style: ListDataStyle;
  /**
   * list of first-level elements
   */
  items: ListItem[];
}
interface NestedListConfig {
  /**
   * default list style: ordered or unordered
   * default is unordered
   */
  defaultStyle?: ListDataStyle;
}

// copied from original code == end ==

// this is also implemented in package but couldn't import from dist source, so I just copied it
function isHtmlElement(node: Node): boolean {
  return node instanceof HTMLElement;
}

// interface for config, 
interface NestedListCustomParams {
  maxDepth?: number;
}

// extending constructor params
export type _NestedListParams = BlockToolConstructorOptions<
  ListData,
  NestedListConfig & NestedListCustomParams
>;

export default class LimitedNestedList extends NestedList {
  constructor({ data, config: _config, api, readOnly }: _NestedListParams) {
    const { maxDepth, ...config } = _config;
    // need to cast to original NestedListParams interface 
    super({ data, config, api, readOnly } as NestedListParams);

    if (!maxDepth || maxDepth < 1) {
      // min : 1 (could nest like : 1.1, 1.2, 2.1, 2.2, ... )
      // default : 2 (could nest like : 1.1.1, 1.2.3, ... )
      this.maxDepth = 2;
      return;
    }
    this.maxDepth = maxDepth;
  }

  private maxDepth: number;

  addTab(event: KeyboardEvent): void {
    // same as original code == start ==
    event.stopPropagation();
    event.preventDefault();

    const currentItem = this.currentItem;
    if (!currentItem) {
      return;
    }
    const prevItem = currentItem.previousSibling;
    if (!prevItem) {
      return;
    }
    if (!isHtmlElement(prevItem)) {
      return;
    }
    const isFirstChild = !prevItem;

    if (isFirstChild) {
      return;
    }
    // same as original code == end ==

    // find until element class name is  : `ce-block__content`
    const maxHTMLDepth = 2 + 3 * (this.maxDepth - 1); // 0, 2, 5, 8, 11, ...
    const rootName = 'ce-block__content';
    let HTMLDepth = 0;
    let element: Element | null = currentItem;
    while (element?.className !== rootName) {
      element = element?.parentElement || null;
      HTMLDepth += 1;
      if (HTMLDepth > maxHTMLDepth) return;
    }

    return super.addTab(event);
  }
}
const nestedListTool = {
  class: LimitedNestedList,
  inlineToolbar: true,
  config: {
    defaultStyle: 'ordered',
  },
};

....

const editorConfig = {
    tools: {
        list : nestedListTool 
        ....
    }
}

I tried to make as finding root element, or Block by using block data

but it was quite hard to override methods, and accessing private variables.

collimarco commented 1 month ago

+1 for this feature: it would be really useful to limit user input to 1 or 2 levels at most