]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/defaults/buttons/objects.ts
Lexical: Ran a deeper check on translation use
[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, $showMediaForm} 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         context.editor.getEditorState().read(() => {
172             const selection = $getSelection();
173             const selectedNode = $getNodeFromSelection(selection, $isMediaNode) as MediaNode | null;
174
175             $showMediaForm(selectedNode, context);
176         });
177     },
178     isActive(selection: BaseSelection | null): boolean {
179         return $selectionContainsNodeType(selection, $isMediaNode);
180     }
181 };
182
183 export const details: EditorButtonDefinition = {
184     label: 'Insert collapsible block',
185     icon: detailsIcon,
186     action(context: EditorUiContext) {
187         context.editor.update(() => {
188             const selection = $getSelection();
189             const detailsNode = $createDetailsNode();
190             const selectionNodes = selection?.getNodes() || [];
191             const topLevels = selectionNodes.map(n => n.getTopLevelElement())
192                 .filter(n => n !== null) as ElementNode[];
193             const uniqueTopLevels = [...new Set(topLevels)];
194
195             if (uniqueTopLevels.length > 0) {
196                 uniqueTopLevels[0].insertAfter(detailsNode);
197             } else {
198                 $getRoot().append(detailsNode);
199             }
200
201             for (const node of uniqueTopLevels) {
202                 detailsNode.append(node);
203             }
204         });
205     },
206     isActive(selection: BaseSelection | null): boolean {
207         return $selectionContainsNodeType(selection, $isDetailsNode);
208     }
209 }
210
211 export const detailsEditLabel: EditorButtonDefinition = {
212     label: 'Edit label',
213     icon: tagIcon,
214     action(context: EditorUiContext) {
215         context.editor.getEditorState().read(() => {
216             const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
217             if ($isDetailsNode(details)) {
218                 $showDetailsForm(details, context);
219             }
220         })
221     },
222     isActive(selection: BaseSelection | null): boolean {
223         return false;
224     }
225 }
226
227 export const detailsToggle: EditorButtonDefinition = {
228     label: 'Toggle open/closed',
229     icon: detailsToggleIcon,
230     action(context: EditorUiContext) {
231         context.editor.update(() => {
232             const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
233             if ($isDetailsNode(details)) {
234                 details.setOpen(!details.getOpen());
235                 context.manager.triggerLayoutUpdate();
236             }
237         })
238     },
239     isActive(selection: BaseSelection | null): boolean {
240         return false;
241     }
242 }
243
244 export const detailsUnwrap: EditorButtonDefinition = {
245     label: 'Unwrap',
246     icon: tableDeleteIcon,
247     action(context: EditorUiContext) {
248         context.editor.update(() => {
249             const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
250             if ($isDetailsNode(details)) {
251                 const children = details.getChildren();
252                 for (const child of children) {
253                     details.insertBefore(child);
254                 }
255                 details.remove();
256                 context.manager.triggerLayoutUpdate();
257             }
258         })
259     },
260     isActive(selection: BaseSelection | null): boolean {
261         return false;
262     }
263 }