]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg-tinymce/config.js
Editors: Added lexical editor for testing
[bookstack] / resources / js / wysiwyg-tinymce / config.js
1 import {register as registerShortcuts} from './shortcuts';
2 import {listen as listenForCommonEvents} from './common-events';
3 import {scrollToQueryString} from './scrolling';
4 import {listenForDragAndPaste} from './drop-paste-handling';
5 import {getPrimaryToolbar, registerAdditionalToolbars} from './toolbars';
6 import {registerCustomIcons} from './icons';
7 import {setupFilters} from './filters';
8
9 import {getPlugin as getCodeeditorPlugin} from './plugin-codeeditor';
10 import {getPlugin as getDrawioPlugin} from './plugin-drawio';
11 import {getPlugin as getCustomhrPlugin} from './plugins-customhr';
12 import {getPlugin as getImagemanagerPlugin} from './plugins-imagemanager';
13 import {getPlugin as getAboutPlugin} from './plugins-about';
14 import {getPlugin as getDetailsPlugin} from './plugins-details';
15 import {getPlugin as getTableAdditionsPlugin} from './plugins-table-additions';
16 import {getPlugin as getTasklistPlugin} from './plugins-tasklist';
17 import {
18     handleTableCellRangeEvents,
19     handleEmbedAlignmentChanges,
20     handleTextDirectionCleaning,
21 } from './fixes';
22
23 const styleFormats = [
24     {title: 'Large Header', format: 'h2', preview: 'color: blue;'},
25     {title: 'Medium Header', format: 'h3'},
26     {title: 'Small Header', format: 'h4'},
27     {title: 'Tiny Header', format: 'h5'},
28     {
29         title: 'Paragraph', format: 'p', exact: true, classes: '',
30     },
31     {title: 'Blockquote', format: 'blockquote'},
32     {
33         title: 'Callouts',
34         items: [
35             {title: 'Information', format: 'calloutinfo'},
36             {title: 'Success', format: 'calloutsuccess'},
37             {title: 'Warning', format: 'calloutwarning'},
38             {title: 'Danger', format: 'calloutdanger'},
39         ],
40     },
41 ];
42
43 const formats = {
44     alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video', classes: 'align-left'},
45     aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video', classes: 'align-center'},
46     alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video', classes: 'align-right'},
47     calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
48     calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
49     calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
50     calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}},
51 };
52
53 const colorMap = [
54     '#BFEDD2', '',
55     '#FBEEB8', '',
56     '#F8CAC6', '',
57     '#ECCAFA', '',
58     '#C2E0F4', '',
59
60     '#2DC26B', '',
61     '#F1C40F', '',
62     '#E03E2D', '',
63     '#B96AD9', '',
64     '#3598DB', '',
65
66     '#169179', '',
67     '#E67E23', '',
68     '#BA372A', '',
69     '#843FA1', '',
70     '#236FA1', '',
71
72     '#ECF0F1', '',
73     '#CED4D9', '',
74     '#95A5A6', '',
75     '#7E8C8D', '',
76     '#34495E', '',
77
78     '#000000', '',
79     '#ffffff', '',
80 ];
81
82 function filePickerCallback(callback, value, meta) {
83     // field_name, url, type, win
84     if (meta.filetype === 'file') {
85         /** @type {EntitySelectorPopup} * */
86         const selector = window.$components.first('entity-selector-popup');
87         const selectionText = this.selection.getContent({format: 'text'}).trim();
88         selector.show(entity => {
89             callback(entity.link, {
90                 text: entity.name,
91                 title: entity.name,
92             });
93         }, {
94             initialValue: selectionText,
95             searchEndpoint: '/search/entity-selector',
96             entityTypes: 'page,book,chapter,bookshelf',
97             entityPermission: 'view',
98         });
99     }
100
101     if (meta.filetype === 'image') {
102         // Show image manager
103         /** @type {ImageManager} * */
104         const imageManager = window.$components.first('image-manager');
105         imageManager.show(image => {
106             callback(image.url, {alt: image.name});
107         }, 'gallery');
108     }
109 }
110
111 /**
112  * @param {WysiwygConfigOptions} options
113  * @return {string[]}
114  */
115 function gatherPlugins(options) {
116     const plugins = [
117         'image',
118         'table',
119         'link',
120         'autolink',
121         'fullscreen',
122         'code',
123         'customhr',
124         'autosave',
125         'lists',
126         'codeeditor',
127         'media',
128         'imagemanager',
129         'about',
130         'details',
131         'tasklist',
132         'tableadditions',
133         options.textDirection === 'rtl' ? 'directionality' : '',
134     ];
135
136     window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin());
137     window.tinymce.PluginManager.add('customhr', getCustomhrPlugin());
138     window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin());
139     window.tinymce.PluginManager.add('about', getAboutPlugin());
140     window.tinymce.PluginManager.add('details', getDetailsPlugin());
141     window.tinymce.PluginManager.add('tasklist', getTasklistPlugin());
142     window.tinymce.PluginManager.add('tableadditions', getTableAdditionsPlugin());
143
144     if (options.drawioUrl) {
145         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
146         plugins.push('drawio');
147     }
148
149     return plugins.filter(plugin => Boolean(plugin));
150 }
151
152 /**
153  * Fetch custom HTML head content nodes from the outer page head
154  * and add them to the given editor document.
155  * @param {Document} editorDoc
156  */
157 function addCustomHeadContent(editorDoc) {
158     const headContentLines = document.head.innerHTML.split('\n');
159     const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
160     const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
161     if (startLineIndex === -1 || endLineIndex === -1) {
162         return;
163     }
164
165     const customHeadHtml = headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
166     const el = editorDoc.createElement('div');
167     el.innerHTML = customHeadHtml;
168
169     editorDoc.head.append(...el.children);
170 }
171
172 /**
173  * @param {WysiwygConfigOptions} options
174  * @return {function(Editor)}
175  */
176 function getSetupCallback(options) {
177     return function setupCallback(editor) {
178         function editorChange() {
179             if (options.darkMode) {
180                 editor.contentDocument.documentElement.classList.add('dark-mode');
181             }
182             window.$events.emit('editor-html-change', '');
183         }
184
185         editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
186         listenForCommonEvents(editor);
187         listenForDragAndPaste(editor, options);
188
189         editor.on('init', () => {
190             editorChange();
191             scrollToQueryString(editor);
192             window.editor = editor;
193             registerShortcuts(editor);
194         });
195
196         editor.on('PreInit', () => {
197             setupFilters(editor);
198         });
199
200         handleEmbedAlignmentChanges(editor);
201         handleTableCellRangeEvents(editor);
202         handleTextDirectionCleaning(editor);
203
204         // Custom handler hook
205         window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
206
207         // Inline code format button
208         editor.ui.registry.addButton('inlinecode', {
209             tooltip: 'Inline code',
210             icon: 'sourcecode',
211             onAction() {
212                 editor.execCommand('mceToggleFormat', false, 'code');
213             },
214         });
215     };
216 }
217
218 /**
219  * @param {WysiwygConfigOptions} options
220  */
221 function getContentStyle(options) {
222     return `
223 html, body, html.dark-mode {
224     background: ${options.darkMode ? '#222' : '#fff'};
225
226 body {
227     padding-left: 15px !important;
228     padding-right: 15px !important; 
229     height: initial !important;
230     margin:0!important; 
231     margin-left: auto! important;
232     margin-right: auto !important;
233     overflow-y: hidden !important;
234 }`.trim().replace('\n', '');
235 }
236
237 /**
238  * @param {WysiwygConfigOptions} options
239  * @return {Object}
240  */
241 export function buildForEditor(options) {
242     // Set language
243     window.tinymce.addI18n(options.language, options.translationMap);
244
245     // BookStack Version
246     const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
247
248     // Return config object
249     return {
250         width: '100%',
251         height: '100%',
252         selector: '#html-editor',
253         cache_suffix: `?version=${version}`,
254         content_css: [
255             window.baseUrl('/dist/styles.css'),
256         ],
257         branding: false,
258         skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
259         body_class: 'page-content',
260         browser_spellcheck: true,
261         relative_urls: false,
262         language: options.language,
263         directionality: options.textDirection,
264         remove_script_host: false,
265         document_base_url: window.baseUrl('/'),
266         end_container_on_empty_block: true,
267         remove_trailing_brs: false,
268         statusbar: false,
269         menubar: false,
270         paste_data_images: false,
271         extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked|style]',
272         automatic_uploads: false,
273         custom_elements: 'doc-root,code-block',
274         valid_children: [
275             '-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]',
276             '+div[pre|img]',
277             '-doc-root[doc-root|#text]',
278             '-li[details]',
279             '+code-block[pre]',
280             '+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div|hr]',
281         ].join(','),
282         plugins: gatherPlugins(options),
283         contextmenu: false,
284         toolbar: getPrimaryToolbar(options),
285         content_style: getContentStyle(options),
286         style_formats: styleFormats,
287         style_formats_merge: false,
288         media_alt_source: false,
289         media_poster: false,
290         formats,
291         table_style_by_css: true,
292         table_use_colgroups: true,
293         file_picker_types: 'file image',
294         color_map: colorMap,
295         file_picker_callback: filePickerCallback,
296         paste_preprocess(plugin, args) {
297             const {content} = args;
298             if (content.indexOf('<img src="file://') !== -1) {
299                 args.content = '';
300             }
301         },
302         init_instance_callback(editor) {
303             addCustomHeadContent(editor.getDoc());
304         },
305         setup(editor) {
306             registerCustomIcons(editor);
307             registerAdditionalToolbars(editor);
308             getSetupCallback(options)(editor);
309         },
310     };
311 }
312
313 /**
314  * @param {WysiwygConfigOptions} options
315  * @return {RawEditorOptions}
316  */
317 export function buildForInput(options) {
318     // Set language
319     window.tinymce.addI18n(options.language, options.translationMap);
320
321     // BookStack Version
322     const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
323
324     // Return config object
325     return {
326         width: '100%',
327         height: '185px',
328         target: options.containerElement,
329         cache_suffix: `?version=${version}`,
330         content_css: [
331             window.baseUrl('/dist/styles.css'),
332         ],
333         branding: false,
334         skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
335         body_class: 'wysiwyg-input',
336         browser_spellcheck: true,
337         relative_urls: false,
338         language: options.language,
339         directionality: options.textDirection,
340         remove_script_host: false,
341         document_base_url: window.baseUrl('/'),
342         end_container_on_empty_block: true,
343         remove_trailing_brs: false,
344         statusbar: false,
345         menubar: false,
346         plugins: 'link autolink lists',
347         contextmenu: false,
348         toolbar: 'bold italic link bullist numlist',
349         content_style: getContentStyle(options),
350         file_picker_types: 'file',
351         valid_elements: 'p,a[href|title|target],ol,ul,li,strong,em,br',
352         file_picker_callback: filePickerCallback,
353         init_instance_callback(editor) {
354             addCustomHeadContent(editor.getDoc());
355
356             editor.contentDocument.documentElement.classList.toggle('dark-mode', options.darkMode);
357         },
358     };
359 }
360
361 /**
362  * @typedef {Object} WysiwygConfigOptions
363  * @property {Element} containerElement
364  * @property {string} language
365  * @property {boolean} darkMode
366  * @property {string} textDirection
367  * @property {string} drawioUrl
368  * @property {int} pageId
369  * @property {Object} translations
370  * @property {Object} translationMap
371  */