]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
Removed use of image-manager/entity-selector window globals
[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         /** @type {EntitySelectorPopup} **/
77         const selector = window.$components.first('entity-selector-popup');
78         selector.show(entity => {
79             callback(entity.link, {
80                 text: entity.name,
81                 title: entity.name,
82             });
83         });
84     }
85
86     if (meta.filetype === 'image') {
87         // Show image manager
88         /** @type {ImageManager} **/
89         const imageManager = window.$components.first('image-manager');
90         imageManager.show(function (image) {
91             callback(image.url, {alt: image.name});
92         }, 'gallery');
93     }
94
95 }
96
97 /**
98  * @param {WysiwygConfigOptions} options
99  * @return {string[]}
100  */
101 function gatherPlugins(options) {
102     const plugins = [
103         "image",
104         "table",
105         "link",
106         "autolink",
107         "fullscreen",
108         "code",
109         "customhr",
110         "autosave",
111         "lists",
112         "codeeditor",
113         "media",
114         "imagemanager",
115         "about",
116         "details",
117         "tasklist",
118         options.textDirection === 'rtl' ? 'directionality' : '',
119     ];
120
121     window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin(options));
122     window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
123     window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
124     window.tinymce.PluginManager.add('about', getAboutPlugin(options));
125     window.tinymce.PluginManager.add('details', getDetailsPlugin(options));
126     window.tinymce.PluginManager.add('tasklist', getTasklistPlugin(options));
127
128     if (options.drawioUrl) {
129         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
130         plugins.push('drawio');
131     }
132
133     return plugins.filter(plugin => Boolean(plugin));
134 }
135
136 /**
137  * Fetch custom HTML head content from the parent page head into the editor.
138  */
139 function fetchCustomHeadContent() {
140     const headContentLines = document.head.innerHTML.split("\n");
141     const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
142     const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
143     if (startLineIndex === -1 || endLineIndex === -1) {
144         return ''
145     }
146     return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
147 }
148
149 /**
150  * Setup a serializer filter for <br> tags to ensure they're not rendered
151  * within code blocks and that we use newlines there instead.
152  * @param {Editor} editor
153  */
154 function setupBrFilter(editor) {
155     editor.serializer.addNodeFilter('br', function(nodes) {
156         for (const node of nodes) {
157             if (node.parent && node.parent.name === 'code') {
158                 const newline = tinymce.html.Node.create('#text');
159                 newline.value = '\n';
160                 node.replace(newline);
161             }
162         }
163     });
164 }
165
166 /**
167  * @param {WysiwygConfigOptions} options
168  * @return {function(Editor)}
169  */
170 function getSetupCallback(options) {
171     return function(editor) {
172         editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
173         listenForCommonEvents(editor);
174         listenForDragAndPaste(editor, options);
175
176         editor.on('init', () => {
177             editorChange();
178             scrollToQueryString(editor);
179             window.editor = editor;
180             registerShortcuts(editor);
181         });
182
183         editor.on('PreInit', () => {
184             setupBrFilter(editor);
185         });
186
187         function editorChange() {
188             const content = editor.getContent();
189             if (options.darkMode) {
190                 editor.contentDocument.documentElement.classList.add('dark-mode');
191             }
192             window.$events.emit('editor-html-change', content);
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
234     // Set language
235     window.tinymce.addI18n(options.language, options.translationMap);
236
237     // BookStack Version
238     const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
239
240     // Return config object
241     return {
242         width: '100%',
243         height: '100%',
244         selector: '#html-editor',
245         cache_suffix: '?version=' + version,
246         content_css: [
247             window.baseUrl('/dist/styles.css'),
248         ],
249         branding: false,
250         skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
251         body_class: 'page-content',
252         browser_spellcheck: true,
253         relative_urls: false,
254         language: options.language,
255         directionality: options.textDirection,
256         remove_script_host: false,
257         document_base_url: window.baseUrl('/'),
258         end_container_on_empty_block: true,
259         remove_trailing_brs: false,
260         statusbar: false,
261         menubar: false,
262         paste_data_images: false,
263         extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked|style]',
264         automatic_uploads: false,
265         custom_elements: 'doc-root,code-block',
266         valid_children: [
267             "-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]",
268             "+div[pre|img]",
269             "-doc-root[doc-root|#text]",
270             "-li[details]",
271             "+code-block[pre]",
272             "+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div]"
273         ].join(','),
274         plugins: gatherPlugins(options),
275         contextmenu: false,
276         toolbar: getPrimaryToolbar(options),
277         content_style: getContentStyle(options),
278         style_formats,
279         style_formats_merge: false,
280         media_alt_source: false,
281         media_poster: false,
282         formats,
283         table_style_by_css: true,
284         table_use_colgroups: true,
285         file_picker_types: 'file image',
286         color_map,
287         file_picker_callback,
288         paste_preprocess(plugin, args) {
289             const content = args.content;
290             if (content.indexOf('<img src="file://') !== -1) {
291                 args.content = '';
292             }
293         },
294         init_instance_callback(editor) {
295             const head = editor.getDoc().querySelector('head');
296             head.innerHTML += fetchCustomHeadContent();
297         },
298         setup(editor) {
299             registerCustomIcons(editor);
300             registerAdditionalToolbars(editor, options);
301             getSetupCallback(options)(editor);
302         },
303     };
304 }
305
306 /**
307  * @typedef {Object} WysiwygConfigOptions
308  * @property {Element} containerElement
309  * @property {string} language
310  * @property {boolean} darkMode
311  * @property {string} textDirection
312  * @property {string} drawioUrl
313  * @property {int} pageId
314  * @property {Object} translations
315  * @property {Object} translationMap
316  */