BookStackApp/BookStack

View on GitHub
resources/js/wysiwyg/nodes/custom-list-item.ts

Summary

Maintainability
A
0 mins
Test Coverage
import {$isListNode, ListItemNode, SerializedListItemNode} from "@lexical/list";
import {EditorConfig} from "lexical/LexicalEditor";
import {DOMExportOutput, LexicalEditor, LexicalNode} from "lexical";

import {el} from "../utils/dom";
import {$isCustomListNode} from "./custom-list";

function updateListItemChecked(
    dom: HTMLElement,
    listItemNode: ListItemNode,
): void {
    // Only set task list attrs for leaf list items
    const shouldBeTaskItem = !$isListNode(listItemNode.getFirstChild());
    dom.classList.toggle('task-list-item', shouldBeTaskItem);
    if (listItemNode.__checked) {
        dom.setAttribute('checked', 'checked');
    } else {
        dom.removeAttribute('checked');
    }
}


export class CustomListItemNode extends ListItemNode {
    static getType(): string {
        return 'custom-list-item';
    }

    static clone(node: CustomListItemNode): CustomListItemNode {
        return new CustomListItemNode(node.__value, node.__checked, node.__key);
    }

    createDOM(config: EditorConfig): HTMLElement {
        const element = document.createElement('li');
        const parent = this.getParent();

        if ($isListNode(parent) && parent.getListType() === 'check') {
            updateListItemChecked(element, this);
        }

        element.value = this.__value;

        if ($hasNestedListWithoutLabel(this)) {
            element.style.listStyle = 'none';
        }

        return element;
    }

    updateDOM(
        prevNode: ListItemNode,
        dom: HTMLElement,
        config: EditorConfig,
    ): boolean {
        const parent = this.getParent();
        if ($isListNode(parent) && parent.getListType() === 'check') {
            updateListItemChecked(dom, this);
        }

        dom.style.listStyle = $hasNestedListWithoutLabel(this) ? 'none' : '';
        // @ts-expect-error - this is always HTMLListItemElement
        dom.value = this.__value;

        return false;
    }

    exportDOM(editor: LexicalEditor): DOMExportOutput {
        const element = this.createDOM(editor._config);
        element.style.textAlign = this.getFormatType();

        if (element.classList.contains('task-list-item')) {
            const input = el('input', {
                type: 'checkbox',
                disabled: 'disabled',
            });
            if (element.hasAttribute('checked')) {
                input.setAttribute('checked', 'checked');
                element.removeAttribute('checked');
            }

            element.prepend(input);
        }

        return {
            element,
        };
    }

    exportJSON(): SerializedListItemNode {
        return {
            ...super.exportJSON(),
            type: 'custom-list-item',
        };
    }
}

function $hasNestedListWithoutLabel(node: CustomListItemNode): boolean {
    const children = node.getChildren();
    let hasLabel = false;
    let hasNestedList = false;

    for (const child of children) {
        if ($isCustomListNode(child)) {
            hasNestedList = true;
        } else if (child.getTextContent().trim().length > 0) {
            hasLabel = true;
        }
    }

    return hasNestedList && !hasLabel;
}

export function $isCustomListItemNode(
    node: LexicalNode | null | undefined,
): node is CustomListItemNode {
    return node instanceof CustomListItemNode;
}

export function $createCustomListItemNode(): CustomListItemNode {
    return new CustomListItemNode();
}