]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/defaults/buttons/objects.ts
6612c0dc4514ce207d591fb360adfc8533edbb6b
[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 detailsToggleIcon from "@icons/editor/details-toggle.svg";
23 import tableDeleteIcon from "@icons/editor/table-delete.svg";
24 import tagIcon from "@icons/tag.svg";
25 import mediaIcon from "@icons/editor/media.svg";
26 import {$createDetailsNode, $isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
27 import {$isMediaNode, MediaNode} from "@lexical/rich-text/LexicalMediaNode";
28 import {
29     $getNodeFromSelection,
30     $insertNewBlockNodeAtSelection,
31     $selectionContainsNodeType, getLastSelection
32 } from "../../../utils/selection";
33 import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams";
34 import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
35 import {$showDetailsForm, $showImageForm, $showLinkForm} from "../forms/objects";
36 import {formatCodeBlock} from "../../../utils/formats";
37
38 export const link: EditorButtonDefinition = {
39     label: 'Insert/edit link',
40     icon: linkIcon,
41     action(context: EditorUiContext) {
42         context.editor.getEditorState().read(() => {
43             const selectedLink = $getNodeFromSelection($getSelection(), $isLinkNode) as LinkNode | null;
44             $showLinkForm(selectedLink, context);
45         });
46     },
47     isActive(selection: BaseSelection | null): boolean {
48         return $selectionContainsNodeType(selection, $isLinkNode);
49     }
50 };
51
52 export const unlink: EditorButtonDefinition = {
53     label: 'Remove link',
54     icon: unlinkIcon,
55     action(context: EditorUiContext) {
56         context.editor.update(() => {
57             const selection = getLastSelection(context.editor);
58             const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode | null;
59
60             if (selectedLink) {
61                 const contents = selectedLink.getChildren().reverse();
62                 for (const child of contents) {
63                     selectedLink.insertAfter(child);
64                 }
65                 selectedLink.remove();
66
67                 contents[contents.length - 1].selectStart();
68
69                 context.manager.triggerFutureStateRefresh();
70             }
71         });
72     },
73     isActive(selection: BaseSelection | null): boolean {
74         return false;
75     }
76 };
77
78
79 export const image: EditorButtonDefinition = {
80     label: 'Insert/Edit Image',
81     icon: imageIcon,
82     action(context: EditorUiContext) {
83         context.editor.getEditorState().read(() => {
84             const selection = getLastSelection(context.editor);
85             const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode | null;
86             if (selectedImage) {
87                 $showImageForm(selectedImage, context);
88                 return;
89             }
90
91             showImageManager((image) => {
92                 context.editor.update(() => {
93                     const link = $createLinkedImageNodeFromImageData(image);
94                     $insertNodes([link]);
95                 });
96             })
97         });
98     },
99     isActive(selection: BaseSelection | null): boolean {
100         return $selectionContainsNodeType(selection, $isImageNode);
101     }
102 };
103
104 export const horizontalRule: EditorButtonDefinition = {
105     label: 'Insert horizontal line',
106     icon: horizontalRuleIcon,
107     action(context: EditorUiContext) {
108         context.editor.update(() => {
109             $insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);
110         });
111     },
112     isActive(selection: BaseSelection | null): boolean {
113         return $selectionContainsNodeType(selection, $isHorizontalRuleNode);
114     }
115 };
116
117 export const codeBlock: EditorButtonDefinition = {
118     label: 'Insert code block',
119     icon: codeBlockIcon,
120     action(context: EditorUiContext) {
121         formatCodeBlock(context.editor);
122     },
123     isActive(selection: BaseSelection | null): boolean {
124         return $selectionContainsNodeType(selection, $isCodeBlockNode);
125     }
126 };
127
128 export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, {
129     label: 'Edit code block',
130     icon: editIcon,
131 });
132
133 export const diagram: EditorButtonDefinition = {
134     label: 'Insert/edit drawing',
135     icon: diagramIcon,
136     action(context: EditorUiContext) {
137         context.editor.getEditorState().read(() => {
138             const selection = getLastSelection(context.editor);
139             const diagramNode = $getNodeFromSelection(selection, $isDiagramNode) as (DiagramNode | null);
140             if (diagramNode === null) {
141                 context.editor.update(() => {
142                     const diagram = $createDiagramNode();
143                     $insertNewBlockNodeAtSelection(diagram, true);
144                     $openDrawingEditorForNode(context, diagram);
145                     diagram.selectStart();
146                 });
147             } else {
148                 $openDrawingEditorForNode(context, diagramNode);
149             }
150         });
151     },
152     isActive(selection: BaseSelection | null): boolean {
153         return $selectionContainsNodeType(selection, $isDiagramNode);
154     }
155 };
156
157 export const diagramManager: EditorButtonDefinition = {
158     label: 'Drawing manager',
159     action(context: EditorUiContext) {
160         showDiagramManagerForInsert(context);
161     },
162     isActive(): boolean {
163         return false;
164     }
165 };
166
167 export const media: EditorButtonDefinition = {
168     label: 'Insert/edit Media',
169     icon: mediaIcon,
170     action(context: EditorUiContext) {
171         const mediaModal = context.manager.createModal('media');
172
173         context.editor.getEditorState().read(() => {
174             const selection = $getSelection();
175             const selectedNode = $getNodeFromSelection(selection, $isMediaNode) as MediaNode | null;
176
177             let formDefaults = {};
178             if (selectedNode) {
179                 const nodeAttrs = selectedNode.getAttributes();
180                 formDefaults = {
181                     src: nodeAttrs.src || nodeAttrs.data || '',
182                     width: nodeAttrs.width,
183                     height: nodeAttrs.height,
184                     embed: '',
185                 }
186             }
187
188             mediaModal.show(formDefaults);
189         });
190     },
191     isActive(selection: BaseSelection | null): boolean {
192         return $selectionContainsNodeType(selection, $isMediaNode);
193     }
194 };
195
196 export const details: EditorButtonDefinition = {
197     label: 'Insert collapsible block',
198     icon: detailsIcon,
199     action(context: EditorUiContext) {
200         context.editor.update(() => {
201             const selection = $getSelection();
202             const detailsNode = $createDetailsNode();
203             const selectionNodes = selection?.getNodes() || [];
204             const topLevels = selectionNodes.map(n => n.getTopLevelElement())
205                 .filter(n => n !== null) as ElementNode[];
206             const uniqueTopLevels = [...new Set(topLevels)];
207
208             if (uniqueTopLevels.length > 0) {
209                 uniqueTopLevels[0].insertAfter(detailsNode);
210             } else {
211                 $getRoot().append(detailsNode);
212             }
213
214             for (const node of uniqueTopLevels) {
215                 detailsNode.append(node);
216             }
217         });
218     },
219     isActive(selection: BaseSelection | null): boolean {
220         return $selectionContainsNodeType(selection, $isDetailsNode);
221     }
222 }
223
224 export const detailsEditLabel: EditorButtonDefinition = {
225     label: 'Edit label',
226     icon: tagIcon,
227     action(context: EditorUiContext) {
228         context.editor.getEditorState().read(() => {
229             const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
230             if ($isDetailsNode(details)) {
231                 $showDetailsForm(details, context);
232             }
233         })
234     },
235     isActive(selection: BaseSelection | null): boolean {
236         return false;
237     }
238 }
239
240 export const detailsToggle: EditorButtonDefinition = {
241     label: 'Toggle open/closed',
242     icon: detailsToggleIcon,
243     action(context: EditorUiContext) {
244         context.editor.update(() => {
245             const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
246             if ($isDetailsNode(details)) {
247                 details.setOpen(!details.getOpen());
248                 context.manager.triggerLayoutUpdate();
249             }
250         })
251     },
252     isActive(selection: BaseSelection | null): boolean {
253         return false;
254     }
255 }
256
257 export const detailsUnwrap: EditorButtonDefinition = {
258     label: 'Unwrap',
259     icon: tableDeleteIcon,
260     action(context: EditorUiContext) {
261         context.editor.update(() => {
262             const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
263             if ($isDetailsNode(details)) {
264                 const children = details.getChildren();
265                 for (const child of children) {
266                     details.insertBefore(child);
267                 }
268                 details.remove();
269                 context.manager.triggerLayoutUpdate();
270             }
271         })
272     },
273     isActive(selection: BaseSelection | null): boolean {
274         return false;
275     }
276 }