]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/defaults/buttons/objects.ts
Lexical: Fixed tiny image resizer on image insert
[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                     link.select();
96                 });
97             })
98         });
99     },
100     isActive(selection: BaseSelection | null): boolean {
101         return $selectionContainsNodeType(selection, $isImageNode);
102     }
103 };
104
105 export const horizontalRule: EditorButtonDefinition = {
106     label: 'Insert horizontal line',
107     icon: horizontalRuleIcon,
108     action(context: EditorUiContext) {
109         context.editor.update(() => {
110             $insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);
111         });
112     },
113     isActive(selection: BaseSelection | null): boolean {
114         return $selectionContainsNodeType(selection, $isHorizontalRuleNode);
115     }
116 };
117
118 export const codeBlock: EditorButtonDefinition = {
119     label: 'Insert code block',
120     icon: codeBlockIcon,
121     action(context: EditorUiContext) {
122         formatCodeBlock(context.editor);
123     },
124     isActive(selection: BaseSelection | null): boolean {
125         return $selectionContainsNodeType(selection, $isCodeBlockNode);
126     }
127 };
128
129 export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, {
130     label: 'Edit code block',
131     icon: editIcon,
132 });
133
134 export const diagram: EditorButtonDefinition = {
135     label: 'Insert/edit drawing',
136     icon: diagramIcon,
137     action(context: EditorUiContext) {
138         context.editor.getEditorState().read(() => {
139             const selection = getLastSelection(context.editor);
140             const diagramNode = $getNodeFromSelection(selection, $isDiagramNode) as (DiagramNode | null);
141             if (diagramNode === null) {
142                 context.editor.update(() => {
143                     const diagram = $createDiagramNode();
144                     $insertNewBlockNodeAtSelection(diagram, true);
145                     $openDrawingEditorForNode(context, diagram);
146                     diagram.selectStart();
147                 });
148             } else {
149                 $openDrawingEditorForNode(context, diagramNode);
150             }
151         });
152     },
153     isActive(selection: BaseSelection | null): boolean {
154         return $selectionContainsNodeType(selection, $isDiagramNode);
155     }
156 };
157
158 export const diagramManager: EditorButtonDefinition = {
159     label: 'Drawing manager',
160     action(context: EditorUiContext) {
161         showDiagramManagerForInsert(context);
162     },
163     isActive(): boolean {
164         return false;
165     }
166 };
167
168 export const media: EditorButtonDefinition = {
169     label: 'Insert/edit media',
170     icon: mediaIcon,
171     action(context: EditorUiContext) {
172         context.editor.getEditorState().read(() => {
173             const selection = $getSelection();
174             const selectedNode = $getNodeFromSelection(selection, $isMediaNode) as MediaNode | null;
175
176             $showMediaForm(selectedNode, context);
177         });
178     },
179     isActive(selection: BaseSelection | null): boolean {
180         return $selectionContainsNodeType(selection, $isMediaNode);
181     }
182 };
183
184 export const details: EditorButtonDefinition = {
185     label: 'Insert collapsible block',
186     icon: detailsIcon,
187     action(context: EditorUiContext) {
188         context.editor.update(() => {
189             const selection = $getSelection();
190             const detailsNode = $createDetailsNode();
191             const selectionNodes = selection?.getNodes() || [];
192             const topLevels = selectionNodes.map(n => n.getTopLevelElement())
193                 .filter(n => n !== null) as ElementNode[];
194             const uniqueTopLevels = [...new Set(topLevels)];
195
196             if (uniqueTopLevels.length > 0) {
197                 uniqueTopLevels[0].insertAfter(detailsNode);
198             } else {
199                 $getRoot().append(detailsNode);
200             }
201
202             for (const node of uniqueTopLevels) {
203                 detailsNode.append(node);
204             }
205         });
206     },
207     isActive(selection: BaseSelection | null): boolean {
208         return $selectionContainsNodeType(selection, $isDetailsNode);
209     }
210 }
211
212 export const detailsEditLabel: EditorButtonDefinition = {
213     label: 'Edit label',
214     icon: tagIcon,
215     action(context: EditorUiContext) {
216         context.editor.getEditorState().read(() => {
217             const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
218             if ($isDetailsNode(details)) {
219                 $showDetailsForm(details, context);
220             }
221         })
222     },
223     isActive(selection: BaseSelection | null): boolean {
224         return false;
225     }
226 }
227
228 export const detailsToggle: EditorButtonDefinition = {
229     label: 'Toggle open/closed',
230     icon: detailsToggleIcon,
231     action(context: EditorUiContext) {
232         context.editor.update(() => {
233             const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
234             if ($isDetailsNode(details)) {
235                 details.setOpen(!details.getOpen());
236                 context.manager.triggerLayoutUpdate();
237             }
238         })
239     },
240     isActive(selection: BaseSelection | null): boolean {
241         return false;
242     }
243 }
244
245 export const detailsUnwrap: EditorButtonDefinition = {
246     label: 'Unwrap',
247     icon: tableDeleteIcon,
248     action(context: EditorUiContext) {
249         context.editor.update(() => {
250             const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
251             if ($isDetailsNode(details)) {
252                 const children = details.getChildren();
253                 for (const child of children) {
254                     details.insertBefore(child);
255                 }
256                 details.remove();
257                 context.manager.triggerLayoutUpdate();
258             }
259         })
260     },
261     isActive(selection: BaseSelection | null): boolean {
262         return false;
263     }
264 }