]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/utils/formats.ts
Lexical: Extracted & merged heading & quote nodes
[bookstack] / resources / js / wysiwyg / utils / formats.ts
1 import {
2     $createParagraphNode,
3     $createTextNode,
4     $getSelection,
5     $insertNodes,
6     $isParagraphNode,
7     LexicalEditor,
8     LexicalNode
9 } from "lexical";
10 import {
11     $getBlockElementNodesInSelection,
12     $getNodeFromSelection,
13     $insertNewBlockNodeAtSelection, $selectionContainsNodeType, $selectSingleNode,
14     $toggleSelectionBlockNodeType,
15     getLastSelection
16 } from "./selection";
17 import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block";
18 import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout";
19 import {insertList, ListNode, ListType, removeList} from "@lexical/list";
20 import {$isCustomListNode} from "../nodes/custom-list";
21 import {$createLinkNode, $isLinkNode} from "@lexical/link";
22 import {$createHeadingNode, $isHeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
23 import {$createQuoteNode, $isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
24
25 const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
26     return $isHeadingNode(node) && node.getTag() === tag;
27 };
28
29 export function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagType) {
30     editor.update(() => {
31         $toggleSelectionBlockNodeType(
32             (node) => $isHeaderNodeOfTag(node, tag),
33             () => $createHeadingNode(tag),
34         )
35     });
36 }
37
38 export function toggleSelectionAsParagraph(editor: LexicalEditor) {
39     editor.update(() => {
40         $toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
41     });
42 }
43
44 export function toggleSelectionAsBlockquote(editor: LexicalEditor) {
45     editor.update(() => {
46         $toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
47     });
48 }
49
50 export function toggleSelectionAsList(editor: LexicalEditor, type: ListType) {
51     editor.getEditorState().read(() => {
52         const selection = $getSelection();
53         const listSelected = $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
54             return $isCustomListNode(node) && (node as ListNode).getListType() === type;
55         });
56
57         if (listSelected) {
58             removeList(editor);
59         } else {
60             insertList(editor, type);
61         }
62     });
63 }
64
65 export function formatCodeBlock(editor: LexicalEditor) {
66     editor.getEditorState().read(() => {
67         const selection = $getSelection();
68         const lastSelection = getLastSelection(editor);
69         const codeBlock = $getNodeFromSelection(lastSelection, $isCodeBlockNode) as (CodeBlockNode | null);
70         if (codeBlock === null) {
71             editor.update(() => {
72                 const codeBlock = $createCodeBlockNode();
73                 codeBlock.setCode(selection?.getTextContent() || '');
74
75                 const selectionNodes = $getBlockElementNodesInSelection(selection);
76                 const firstSelectionNode = selectionNodes[0];
77                 const extraNodes = selectionNodes.slice(1);
78                 if (firstSelectionNode) {
79                     firstSelectionNode.replace(codeBlock);
80                     extraNodes.forEach(n => n.remove());
81                 } else {
82                     $insertNewBlockNodeAtSelection(codeBlock, true);
83                 }
84
85                 $openCodeEditorForNode(editor, codeBlock);
86                 $selectSingleNode(codeBlock);
87             });
88         } else {
89             $openCodeEditorForNode(editor, codeBlock);
90         }
91     });
92 }
93
94 export function cycleSelectionCalloutFormats(editor: LexicalEditor) {
95     editor.update(() => {
96         const selection = $getSelection();
97         const blocks = $getBlockElementNodesInSelection(selection);
98
99         let created = false;
100         for (const block of blocks) {
101             if (!$isCalloutNode(block)) {
102                 block.replace($createCalloutNode('info'), true);
103                 created = true;
104             }
105         }
106
107         if (created) {
108             return;
109         }
110
111         const types: CalloutCategory[] = ['info', 'warning', 'danger', 'success'];
112         for (const block of blocks) {
113             if ($isCalloutNode(block)) {
114                 const type = block.getCategory();
115                 const typeIndex = types.indexOf(type);
116                 const newIndex = (typeIndex + 1) % types.length;
117                 const newType = types[newIndex];
118                 block.setCategory(newType);
119             }
120         }
121     });
122 }
123
124 export function insertOrUpdateLink(editor: LexicalEditor, linkDetails: {text: string, title: string, target: string, url: string}) {
125     editor.update(() => {
126         const selection = $getSelection();
127         let link = $getNodeFromSelection(selection, $isLinkNode);
128         if ($isLinkNode(link)) {
129             link.setURL(linkDetails.url);
130             link.setTarget(linkDetails.target);
131             link.setTitle(linkDetails.title);
132         } else {
133             link = $createLinkNode(linkDetails.url, {
134                 title: linkDetails.title,
135                 target: linkDetails.target,
136             });
137
138             $insertNodes([link]);
139         }
140
141         if ($isLinkNode(link)) {
142             for (const child of link.getChildren()) {
143                 child.remove(true);
144             }
145             link.append($createTextNode(linkDetails.text));
146         }
147     });
148 }