]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/defaults/forms/objects.ts
Lexical: Added mobile toolbar support
[bookstack] / resources / js / wysiwyg / ui / defaults / forms / objects.ts
1 import {
2     EditorFormDefinition,
3     EditorFormField,
4     EditorFormTabs,
5     EditorSelectFormFieldDefinition
6 } from "../../framework/forms";
7 import {EditorUiContext} from "../../framework/core";
8 import {$createNodeSelection, $getSelection, $insertNodes, $setSelection} from "lexical";
9 import {$isImageNode, ImageNode} from "@lexical/rich-text/LexicalImageNode";
10 import {LinkNode} from "@lexical/link";
11 import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "@lexical/rich-text/LexicalMediaNode";
12 import {$getNodeFromSelection, getLastSelection} from "../../../utils/selection";
13 import {EditorFormModal} from "../../framework/modals";
14 import {EditorActionField} from "../../framework/blocks/action-field";
15 import {EditorButton} from "../../framework/buttons";
16 import {showImageManager} from "../../../utils/images";
17 import searchImageIcon from "@icons/editor/image-search.svg";
18 import searchIcon from "@icons/search.svg";
19 import {showLinkSelector} from "../../../utils/links";
20 import {LinkField} from "../../framework/blocks/link-field";
21 import {insertOrUpdateLink} from "../../../utils/formats";
22
23 export function $showImageForm(image: ImageNode, context: EditorUiContext) {
24     const imageModal: EditorFormModal = context.manager.createModal('image');
25     const height = image.getHeight();
26     const width = image.getWidth();
27
28     const formData = {
29         src: image.getSrc(),
30         alt: image.getAltText(),
31         height: height === 0 ? '' : String(height),
32         width: width === 0 ? '' : String(width),
33     };
34
35     imageModal.show(formData);
36 }
37
38 export const image: EditorFormDefinition = {
39     submitText: 'Apply',
40     async action(formData, context: EditorUiContext) {
41         context.editor.update(() => {
42             const selection = getLastSelection(context.editor);
43             const selectedImage = $getNodeFromSelection(selection, $isImageNode);
44             if ($isImageNode(selectedImage)) {
45                 selectedImage.setSrc(formData.get('src')?.toString() || '');
46                 selectedImage.setAltText(formData.get('alt')?.toString() || '');
47
48                 selectedImage.setWidth(Number(formData.get('width')?.toString() || '0'));
49                 selectedImage.setHeight(Number(formData.get('height')?.toString() || '0'));
50             }
51         });
52         return true;
53     },
54     fields: [
55         {
56             build() {
57                 return new EditorActionField(
58                     new EditorFormField({
59                         label: 'Source',
60                         name: 'src',
61                         type: 'text',
62                     }),
63                     new EditorButton({
64                         label: 'Browse files',
65                         icon: searchImageIcon,
66                         action(context: EditorUiContext) {
67                             showImageManager((image) => {
68                                  const modal =  context.manager.getActiveModal('image');
69                                  if (modal) {
70                                      modal.getForm().setValues({
71                                          src: image.thumbs?.display || image.url,
72                                          alt: image.name,
73                                      });
74                                  }
75                             });
76                         }
77                     }),
78                 );
79             },
80         },
81         {
82             label: 'Alternative description',
83             name: 'alt',
84             type: 'text',
85         },
86         {
87             label: 'Width',
88             name: 'width',
89             type: 'text',
90         },
91         {
92             label: 'Height',
93             name: 'height',
94             type: 'text',
95         },
96     ],
97 };
98
99 export function $showLinkForm(link: LinkNode|null, context: EditorUiContext) {
100     const linkModal = context.manager.createModal('link');
101
102     if (link) {
103         const formDefaults: Record<string, string> = {
104             url: link.getURL(),
105             text: link.getTextContent(),
106             title: link.getTitle() || '',
107             target: link.getTarget() || '',
108         }
109
110         context.editor.update(() => {
111             const selection = $createNodeSelection();
112             selection.add(link.getKey());
113             $setSelection(selection);
114         });
115
116         linkModal.show(formDefaults);
117     } else {
118         context.editor.getEditorState().read(() => {
119             const selection = $getSelection();
120             const text = selection?.getTextContent() || '';
121             const formDefaults = {text};
122             linkModal.show(formDefaults);
123         });
124     }
125 }
126
127 export const link: EditorFormDefinition = {
128     submitText: 'Apply',
129     async action(formData, context: EditorUiContext) {
130         insertOrUpdateLink(context.editor, {
131             url: formData.get('url')?.toString() || '',
132             title: formData.get('title')?.toString() || '',
133             target: formData.get('target')?.toString() || '',
134             text: formData.get('text')?.toString() || '',
135         });
136         return true;
137     },
138     fields: [
139         {
140             build() {
141                 return new EditorActionField(
142                     new LinkField(new EditorFormField({
143                         label: 'URL',
144                         name: 'url',
145                         type: 'text',
146                     })),
147                     new EditorButton({
148                         label: 'Browse links',
149                         icon: searchIcon,
150                         action(context: EditorUiContext) {
151                             showLinkSelector(entity => {
152                                 const modal =  context.manager.getActiveModal('link');
153                                 if (modal) {
154                                     modal.getForm().setValues({
155                                         url: entity.link,
156                                         text: entity.name,
157                                         title: entity.name,
158                                     });
159                                 }
160                             });
161                         }
162                     }),
163                 );
164             },
165         },
166         {
167             label: 'Text to display',
168             name: 'text',
169             type: 'text',
170         },
171         {
172             label: 'Title',
173             name: 'title',
174             type: 'text',
175         },
176         {
177             label: 'Open link in...',
178             name: 'target',
179             type: 'select',
180             valuesByLabel: {
181                 'Current window': '',
182                 'New window': '_blank',
183             }
184         } as EditorSelectFormFieldDefinition,
185     ],
186 };
187
188 export const media: EditorFormDefinition = {
189     submitText: 'Save',
190     async action(formData, context: EditorUiContext) {
191         const selectedNode: MediaNode|null = await (new Promise((res, rej) => {
192             context.editor.getEditorState().read(() => {
193                 const node = $getNodeFromSelection($getSelection(), $isMediaNode);
194                 res(node as MediaNode|null);
195             });
196         }));
197
198         const embedCode = (formData.get('embed') || '').toString().trim();
199         if (embedCode) {
200             context.editor.update(() => {
201                 const node = $createMediaNodeFromHtml(embedCode);
202                 if (selectedNode && node) {
203                     selectedNode.replace(node)
204                 } else if (node) {
205                     $insertNodes([node]);
206                 }
207             });
208
209             return true;
210         }
211
212         context.editor.update(() => {
213             const src = (formData.get('src') || '').toString().trim();
214             const height = (formData.get('height') || '').toString().trim();
215             const width = (formData.get('width') || '').toString().trim();
216
217             const updateNode = selectedNode || $createMediaNodeFromSrc(src);
218             updateNode.setSrc(src);
219             updateNode.setWidthAndHeight(width, height);
220             if (!selectedNode) {
221                 $insertNodes([updateNode]);
222             }
223         });
224
225         return true;
226     },
227     fields: [
228         {
229             build() {
230                 return new EditorFormTabs([
231                     {
232                         label: 'General',
233                         contents: [
234                             {
235                                 label: 'Source',
236                                 name: 'src',
237                                 type: 'text',
238                             },
239                             {
240                                 label: 'Width',
241                                 name: 'width',
242                                 type: 'text',
243                             },
244                             {
245                                 label: 'Height',
246                                 name: 'height',
247                                 type: 'text',
248                             },
249                         ],
250                     },
251                     {
252                         label: 'Embed',
253                         contents: [
254                             {
255                                 label: 'Paste your embed code below:',
256                                 name: 'embed',
257                                 type: 'textarea',
258                             },
259                         ],
260                     }
261                 ])
262             }
263         },
264     ],
265 };