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