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