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