]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/defaults/buttons/objects.ts
Lexical: Reorganised custom node code into lexical codebase
[bookstack] / resources / js / wysiwyg / ui / defaults / buttons / objects.ts
1 import {EditorButtonDefinition} from "../../framework/buttons";
2 import linkIcon from "@icons/editor/link.svg";
3 import {EditorUiContext} from "../../framework/core";
4 import {
5     $getRoot,
6     $getSelection, $insertNodes,
7     BaseSelection,
8     ElementNode
9 } from "lexical";
10 import {$isLinkNode, LinkNode} from "@lexical/link";
11 import unlinkIcon from "@icons/editor/unlink.svg";
12 import imageIcon from "@icons/editor/image.svg";
13 import {$isImageNode, ImageNode} from "@lexical/rich-text/LexicalImageNode";
14 import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg";
15 import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "@lexical/rich-text/LexicalHorizontalRuleNode";
16 import codeBlockIcon from "@icons/editor/code-block.svg";
17 import {$isCodeBlockNode} from "@lexical/rich-text/LexicalCodeBlockNode";
18 import editIcon from "@icons/edit.svg";
19 import diagramIcon from "@icons/editor/diagram.svg";
20 import {$createDiagramNode, DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
21 import detailsIcon from "@icons/editor/details.svg";
22 import mediaIcon from "@icons/editor/media.svg";
23 import {$createDetailsNode, $isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
24 import {$isMediaNode, MediaNode} from "@lexical/rich-text/LexicalMediaNode";
25 import {
26     $getNodeFromSelection,
27     $insertNewBlockNodeAtSelection,
28     $selectionContainsNodeType, getLastSelection
29 } from "../../../utils/selection";
30 import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams";
31 import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
32 import {$showImageForm, $showLinkForm} from "../forms/objects";
33 import {formatCodeBlock} from "../../../utils/formats";
34
35 export const link: EditorButtonDefinition = {
36     label: 'Insert/edit link',
37     icon: linkIcon,
38     action(context: EditorUiContext) {
39         context.editor.getEditorState().read(() => {
40             const selectedLink = $getNodeFromSelection($getSelection(), $isLinkNode) as LinkNode | null;
41             $showLinkForm(selectedLink, context);
42         });
43     },
44     isActive(selection: BaseSelection | null): boolean {
45         return $selectionContainsNodeType(selection, $isLinkNode);
46     }
47 };
48
49 export const unlink: EditorButtonDefinition = {
50     label: 'Remove link',
51     icon: unlinkIcon,
52     action(context: EditorUiContext) {
53         context.editor.update(() => {
54             const selection = getLastSelection(context.editor);
55             const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode | null;
56
57             if (selectedLink) {
58                 const contents = selectedLink.getChildren().reverse();
59                 for (const child of contents) {
60                     selectedLink.insertAfter(child);
61                 }
62                 selectedLink.remove();
63
64                 contents[contents.length - 1].selectStart();
65
66                 context.manager.triggerFutureStateRefresh();
67             }
68         });
69     },
70     isActive(selection: BaseSelection | null): boolean {
71         return false;
72     }
73 };
74
75
76 export const image: EditorButtonDefinition = {
77     label: 'Insert/Edit Image',
78     icon: imageIcon,
79     action(context: EditorUiContext) {
80         context.editor.getEditorState().read(() => {
81             const selection = getLastSelection(context.editor);
82             const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode | null;
83             if (selectedImage) {
84                 $showImageForm(selectedImage, context);
85                 return;
86             }
87
88             showImageManager((image) => {
89                 context.editor.update(() => {
90                     const link = $createLinkedImageNodeFromImageData(image);
91                     $insertNodes([link]);
92                 });
93             })
94         });
95     },
96     isActive(selection: BaseSelection | null): boolean {
97         return $selectionContainsNodeType(selection, $isImageNode);
98     }
99 };
100
101 export const horizontalRule: EditorButtonDefinition = {
102     label: 'Insert horizontal line',
103     icon: horizontalRuleIcon,
104     action(context: EditorUiContext) {
105         context.editor.update(() => {
106             $insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);
107         });
108     },
109     isActive(selection: BaseSelection | null): boolean {
110         return $selectionContainsNodeType(selection, $isHorizontalRuleNode);
111     }
112 };
113
114 export const codeBlock: EditorButtonDefinition = {
115     label: 'Insert code block',
116     icon: codeBlockIcon,
117     action(context: EditorUiContext) {
118         formatCodeBlock(context.editor);
119     },
120     isActive(selection: BaseSelection | null): boolean {
121         return $selectionContainsNodeType(selection, $isCodeBlockNode);
122     }
123 };
124
125 export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, {
126     label: 'Edit code block',
127     icon: editIcon,
128 });
129
130 export const diagram: EditorButtonDefinition = {
131     label: 'Insert/edit drawing',
132     icon: diagramIcon,
133     action(context: EditorUiContext) {
134         context.editor.getEditorState().read(() => {
135             const selection = getLastSelection(context.editor);
136             const diagramNode = $getNodeFromSelection(selection, $isDiagramNode) as (DiagramNode | null);
137             if (diagramNode === null) {
138                 context.editor.update(() => {
139                     const diagram = $createDiagramNode();
140                     $insertNewBlockNodeAtSelection(diagram, true);
141                     $openDrawingEditorForNode(context, diagram);
142                     diagram.selectStart();
143                 });
144             } else {
145                 $openDrawingEditorForNode(context, diagramNode);
146             }
147         });
148     },
149     isActive(selection: BaseSelection | null): boolean {
150         return $selectionContainsNodeType(selection, $isDiagramNode);
151     }
152 };
153
154 export const diagramManager: EditorButtonDefinition = {
155     label: 'Drawing manager',
156     action(context: EditorUiContext) {
157         showDiagramManagerForInsert(context);
158     },
159     isActive(): boolean {
160         return false;
161     }
162 };
163
164 export const media: EditorButtonDefinition = {
165     label: 'Insert/edit Media',
166     icon: mediaIcon,
167     action(context: EditorUiContext) {
168         const mediaModal = context.manager.createModal('media');
169
170         context.editor.getEditorState().read(() => {
171             const selection = $getSelection();
172             const selectedNode = $getNodeFromSelection(selection, $isMediaNode) as MediaNode | null;
173
174             let formDefaults = {};
175             if (selectedNode) {
176                 const nodeAttrs = selectedNode.getAttributes();
177                 formDefaults = {
178                     src: nodeAttrs.src || nodeAttrs.data || '',
179                     width: nodeAttrs.width,
180                     height: nodeAttrs.height,
181                     embed: '',
182                 }
183             }
184
185             mediaModal.show(formDefaults);
186         });
187     },
188     isActive(selection: BaseSelection | null): boolean {
189         return $selectionContainsNodeType(selection, $isMediaNode);
190     }
191 };
192
193 export const details: EditorButtonDefinition = {
194     label: 'Insert collapsible block',
195     icon: detailsIcon,
196     action(context: EditorUiContext) {
197         context.editor.update(() => {
198             const selection = $getSelection();
199             const detailsNode = $createDetailsNode();
200             const selectionNodes = selection?.getNodes() || [];
201             const topLevels = selectionNodes.map(n => n.getTopLevelElement())
202                 .filter(n => n !== null) as ElementNode[];
203             const uniqueTopLevels = [...new Set(topLevels)];
204
205             if (uniqueTopLevels.length > 0) {
206                 uniqueTopLevels[0].insertAfter(detailsNode);
207             } else {
208                 $getRoot().append(detailsNode);
209             }
210
211             for (const node of uniqueTopLevels) {
212                 detailsNode.append(node);
213             }
214         });
215     },
216     isActive(selection: BaseSelection | null): boolean {
217         return $selectionContainsNodeType(selection, $isDetailsNode);
218     }
219 }