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