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