BookStack/resources/js/wysiwyg/nodes/custom-list-item.ts
Dan Brown 662110c269
Lexical: Custom list nesting support
Added list nesting support to allow li > ul style nesting which lexical
didn't do by default.
Adds tab handling for inset/outset controls.
Will be a range of edge-case bugs to squash during testing.
2024-09-13 15:50:42 +01:00

118 lines
3.3 KiB
TypeScript

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);
}
// @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();
}