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