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