]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/custom-list.ts
Lexical: Extracted & merged heading & quote nodes
[bookstack] / resources / js / wysiwyg / nodes / custom-list.ts
1 import {
2     DOMConversionFn,
3     DOMConversionMap, EditorConfig,
4     LexicalNode,
5     Spread
6 } from "lexical";
7 import {$isListItemNode, ListItemNode, ListNode, ListType, SerializedListNode} from "@lexical/list";
8 import {$createCustomListItemNode} from "./custom-list-item";
9 import {extractDirectionFromElement} from "./_common";
10
11
12 export type SerializedCustomListNode = Spread<{
13     id: string;
14 }, SerializedListNode>
15
16 export class CustomListNode extends ListNode {
17     __id: string = '';
18
19     static getType() {
20         return 'custom-list';
21     }
22
23     setId(id: string) {
24         const self = this.getWritable();
25         self.__id = id;
26     }
27
28     getId(): string {
29         const self = this.getLatest();
30         return self.__id;
31     }
32
33     static clone(node: CustomListNode) {
34         const newNode = new CustomListNode(node.__listType, node.__start, node.__key);
35         newNode.__id = node.__id;
36         newNode.__dir = node.__dir;
37         return newNode;
38     }
39
40     createDOM(config: EditorConfig): HTMLElement {
41         const dom = super.createDOM(config);
42         if (this.__id) {
43             dom.setAttribute('id', this.__id);
44         }
45
46         if (this.__dir) {
47             dom.setAttribute('dir', this.__dir);
48         }
49
50         return dom;
51     }
52
53     updateDOM(prevNode: ListNode, dom: HTMLElement, config: EditorConfig): boolean {
54         return super.updateDOM(prevNode, dom, config) ||
55             prevNode.__dir !== this.__dir;
56     }
57
58     exportJSON(): SerializedCustomListNode {
59         return {
60             ...super.exportJSON(),
61             type: 'custom-list',
62             version: 1,
63             id: this.__id,
64         };
65     }
66
67     static importJSON(serializedNode: SerializedCustomListNode): CustomListNode {
68         const node = $createCustomListNode(serializedNode.listType);
69         node.setId(serializedNode.id);
70         node.setDirection(serializedNode.direction);
71         return node;
72     }
73
74     static importDOM(): DOMConversionMap | null {
75         // @ts-ignore
76         const converter = super.importDOM().ol().conversion as DOMConversionFn<HTMLElement>;
77         const customConvertFunction = (element: HTMLElement) => {
78             const baseResult = converter(element);
79             if (element.id && baseResult?.node) {
80                 (baseResult.node as CustomListNode).setId(element.id);
81             }
82
83             if (element.dir && baseResult?.node) {
84                 (baseResult.node as CustomListNode).setDirection(extractDirectionFromElement(element));
85             }
86
87             if (baseResult) {
88                 baseResult.after = $normalizeChildren;
89             }
90
91             return baseResult;
92         };
93
94         return {
95             ol: () => ({
96                 conversion: customConvertFunction,
97                 priority: 0,
98             }),
99             ul: () => ({
100                 conversion: customConvertFunction,
101                 priority: 0,
102             }),
103         };
104     }
105 }
106
107 /*
108  * This function is a custom normalization function to allow nested lists within list item elements.
109  * Original taken from https://github.com/facebook/lexical/blob/6e10210fd1e113ccfafdc999b1d896733c5c5bea/packages/lexical-list/src/LexicalListNode.ts#L284-L303
110  * With modifications made.
111  * Copyright (c) Meta Platforms, Inc. and affiliates.
112  * MIT license
113  */
114 function $normalizeChildren(nodes: Array<LexicalNode>): Array<ListItemNode> {
115     const normalizedListItems: Array<ListItemNode> = [];
116
117     for (const node of nodes) {
118         if ($isListItemNode(node)) {
119             normalizedListItems.push(node);
120         } else {
121             normalizedListItems.push($wrapInListItem(node));
122         }
123     }
124
125     return normalizedListItems;
126 }
127
128 function $wrapInListItem(node: LexicalNode): ListItemNode {
129     const listItemWrapper = $createCustomListItemNode();
130     return listItemWrapper.append(node);
131 }
132
133 export function $createCustomListNode(type: ListType): CustomListNode {
134     return new CustomListNode(type, 1);
135 }
136
137 export function $isCustomListNode(node: LexicalNode | null | undefined): node is CustomListNode {
138     return node instanceof CustomListNode;
139 }