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