]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/defaults/forms/objects.ts
Opensearch: Fixed XML declaration when php short tags enabled
[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 import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
23
24 export function $showImageForm(image: ImageNode, context: EditorUiContext) {
25     const imageModal: EditorFormModal = context.manager.createModal('image');
26     const height = image.getHeight();
27     const width = image.getWidth();
28
29     const formData = {
30         src: image.getSrc(),
31         alt: image.getAltText(),
32         height: height === 0 ? '' : String(height),
33         width: width === 0 ? '' : String(width),
34     };
35
36     imageModal.show(formData);
37 }
38
39 export const image: EditorFormDefinition = {
40     submitText: 'Apply',
41     async action(formData, context: EditorUiContext) {
42         context.editor.update(() => {
43             const selection = getLastSelection(context.editor);
44             const selectedImage = $getNodeFromSelection(selection, $isImageNode);
45             if ($isImageNode(selectedImage)) {
46                 selectedImage.setSrc(formData.get('src')?.toString() || '');
47                 selectedImage.setAltText(formData.get('alt')?.toString() || '');
48
49                 selectedImage.setWidth(Number(formData.get('width')?.toString() || '0'));
50                 selectedImage.setHeight(Number(formData.get('height')?.toString() || '0'));
51             }
52         });
53         return true;
54     },
55     fields: [
56         {
57             build() {
58                 return new EditorActionField(
59                     new EditorFormField({
60                         label: 'Source',
61                         name: 'src',
62                         type: 'text',
63                     }),
64                     new EditorButton({
65                         label: 'Browse files',
66                         icon: searchImageIcon,
67                         action(context: EditorUiContext) {
68                             showImageManager((image) => {
69                                  const modal =  context.manager.getActiveModal('image');
70                                  if (modal) {
71                                      modal.getForm().setValues({
72                                          src: image.thumbs?.display || image.url,
73                                          alt: image.name,
74                                      });
75                                  }
76                             });
77                         }
78                     }),
79                 );
80             },
81         },
82         {
83             label: 'Alternative description',
84             name: 'alt',
85             type: 'text',
86         },
87         {
88             label: 'Width',
89             name: 'width',
90             type: 'text',
91         },
92         {
93             label: 'Height',
94             name: 'height',
95             type: 'text',
96         },
97     ],
98 };
99
100 export function $showLinkForm(link: LinkNode|null, context: EditorUiContext) {
101     const linkModal = context.manager.createModal('link');
102
103     if (link) {
104         const formDefaults: Record<string, string> = {
105             url: link.getURL(),
106             text: link.getTextContent(),
107             title: link.getTitle() || '',
108             target: link.getTarget() || '',
109         }
110
111         context.editor.update(() => {
112             const selection = $createNodeSelection();
113             selection.add(link.getKey());
114             $setSelection(selection);
115         });
116
117         linkModal.show(formDefaults);
118     } else {
119         context.editor.getEditorState().read(() => {
120             const selection = $getSelection();
121             const text = selection?.getTextContent() || '';
122             const formDefaults = {text};
123             linkModal.show(formDefaults);
124         });
125     }
126 }
127
128 export const link: EditorFormDefinition = {
129     submitText: 'Apply',
130     async action(formData, context: EditorUiContext) {
131         insertOrUpdateLink(context.editor, {
132             url: formData.get('url')?.toString() || '',
133             title: formData.get('title')?.toString() || '',
134             target: formData.get('target')?.toString() || '',
135             text: formData.get('text')?.toString() || '',
136         });
137         return true;
138     },
139     fields: [
140         {
141             build() {
142                 return new EditorActionField(
143                     new LinkField(new EditorFormField({
144                         label: 'URL',
145                         name: 'url',
146                         type: 'text',
147                     })),
148                     new EditorButton({
149                         label: 'Browse links',
150                         icon: searchIcon,
151                         action(context: EditorUiContext) {
152                             showLinkSelector(entity => {
153                                 const modal =  context.manager.getActiveModal('link');
154                                 if (modal) {
155                                     modal.getForm().setValues({
156                                         url: entity.link,
157                                         text: entity.name,
158                                         title: entity.name,
159                                     });
160                                 }
161                             });
162                         }
163                     }),
164                 );
165             },
166         },
167         {
168             label: 'Text to display',
169             name: 'text',
170             type: 'text',
171         },
172         {
173             label: 'Title',
174             name: 'title',
175             type: 'text',
176         },
177         {
178             label: 'Open link in...',
179             name: 'target',
180             type: 'select',
181             valuesByLabel: {
182                 'Current window': '',
183                 'New window': '_blank',
184             }
185         } as EditorSelectFormFieldDefinition,
186     ],
187 };
188
189 export function $showMediaForm(media: MediaNode|null, context: EditorUiContext): void {
190     const mediaModal = context.manager.createModal('media');
191
192     let formDefaults = {};
193     if (media) {
194         const nodeAttrs = media.getAttributes();
195         const nodeDOM = media.exportDOM(context.editor).element;
196         const nodeHtml = (nodeDOM instanceof HTMLElement) ? nodeDOM.outerHTML : '';
197
198         formDefaults = {
199             src: nodeAttrs.src || nodeAttrs.data || media.getSources()[0]?.src || '',
200             width: nodeAttrs.width,
201             height: nodeAttrs.height,
202             embed: nodeHtml,
203
204             // This is used so we can check for edits against the embed field on submit
205             embed_check: nodeHtml,
206         }
207     }
208
209     mediaModal.show(formDefaults);
210 }
211
212 export const media: EditorFormDefinition = {
213     submitText: 'Save',
214     async action(formData, context: EditorUiContext) {
215         const selectedNode: MediaNode|null = await (new Promise((res, rej) => {
216             context.editor.getEditorState().read(() => {
217                 const node = $getNodeFromSelection($getSelection(), $isMediaNode);
218                 res(node as MediaNode|null);
219             });
220         }));
221
222         const embedCode = (formData.get('embed') || '').toString().trim();
223         const embedCheck = (formData.get('embed_check') || '').toString().trim();
224         if (embedCode && embedCode !== embedCheck) {
225             context.editor.update(() => {
226                 const node = $createMediaNodeFromHtml(embedCode);
227                 if (selectedNode && node) {
228                     selectedNode.replace(node)
229                 } else if (node) {
230                     $insertNodes([node]);
231                 }
232             });
233
234             return true;
235         }
236
237         context.editor.update(() => {
238             const src = (formData.get('src') || '').toString().trim();
239             const height = (formData.get('height') || '').toString().trim();
240             const width = (formData.get('width') || '').toString().trim();
241
242             // Update existing
243             if (selectedNode) {
244                 selectedNode.setSrc(src);
245                 selectedNode.setWidthAndHeight(width, height);
246                 context.manager.triggerFutureStateRefresh();
247                 return;
248             }
249
250             // Insert new
251             const node = $createMediaNodeFromSrc(src);
252             if (width || height) {
253                 node.setWidthAndHeight(width, height);
254             }
255             $insertNodes([node]);
256         });
257
258         return true;
259     },
260     fields: [
261         {
262             build() {
263                 return new EditorFormTabs([
264                     {
265                         label: 'General',
266                         contents: [
267                             {
268                                 label: 'Source',
269                                 name: 'src',
270                                 type: 'text',
271                             },
272                             {
273                                 label: 'Width',
274                                 name: 'width',
275                                 type: 'text',
276                             },
277                             {
278                                 label: 'Height',
279                                 name: 'height',
280                                 type: 'text',
281                             },
282                         ],
283                     },
284                     {
285                         label: 'Embed',
286                         contents: [
287                             {
288                                 label: 'Paste your embed code below:',
289                                 name: 'embed',
290                                 type: 'textarea',
291                             },
292                             {
293                                 label: '',
294                                 name: 'embed_check',
295                                 type: 'hidden',
296                             },
297                         ],
298                     }
299                 ])
300             }
301         },
302     ],
303 };
304
305 export function $showDetailsForm(details: DetailsNode|null, context: EditorUiContext) {
306     const linkModal = context.manager.createModal('details');
307     if (!details) {
308         return;
309     }
310
311     linkModal.show({
312         summary: details.getSummary()
313     });
314 }
315
316 export const details: EditorFormDefinition = {
317     submitText: 'Save',
318     async action(formData, context: EditorUiContext) {
319         context.editor.update(() => {
320             const node = $getNodeFromSelection($getSelection(), $isDetailsNode);
321             const summary = (formData.get('summary') || '').toString().trim();
322             if ($isDetailsNode(node)) {
323                 node.setSummary(summary);
324             }
325         });
326
327         return true;
328     },
329     fields: [
330         {
331             label: 'Toggle label',
332             name: 'summary',
333             type: 'text',
334         },
335     ],
336 };