]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
Added examples, updated docs for image gallery api endpoints
[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             if (options.darkMode) {
189                 editor.contentDocument.documentElement.classList.add('dark-mode');
190             }
191             window.$events.emit('editor-html-change', '');
192         }
193
194         // Custom handler hook
195         window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
196
197         // Inline code format button
198         editor.ui.registry.addButton('inlinecode', {
199             tooltip: 'Inline code',
200             icon: 'sourcecode',
201             onAction() {
202                 editor.execCommand('mceToggleFormat', false, 'code');
203             }
204         })
205     }
206 }
207
208 /**
209  * @param {WysiwygConfigOptions} options
210  */
211 function getContentStyle(options) {
212     return `
213 html, body, html.dark-mode {
214     background: ${options.darkMode ? '#222' : '#fff'};
215
216 body {
217     padding-left: 15px !important;
218     padding-right: 15px !important; 
219     height: initial !important;
220     margin:0!important; 
221     margin-left: auto! important;
222     margin-right: auto !important;
223     overflow-y: hidden !important;
224 }`.trim().replace('\n', '');
225 }
226
227 /**
228  * @param {WysiwygConfigOptions} options
229  * @return {Object}
230  */
231 export function build(options) {
232
233     // Set language
234     window.tinymce.addI18n(options.language, options.translationMap);
235
236     // BookStack Version
237     const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
238
239     // Return config object
240     return {
241         width: '100%',
242         height: '100%',
243         selector: '#html-editor',
244         cache_suffix: '?version=' + version,
245         content_css: [
246             window.baseUrl('/dist/styles.css'),
247         ],
248         branding: false,
249         skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
250         body_class: 'page-content',
251         browser_spellcheck: true,
252         relative_urls: false,
253         language: options.language,
254         directionality: options.textDirection,
255         remove_script_host: false,
256         document_base_url: window.baseUrl('/'),
257         end_container_on_empty_block: true,
258         remove_trailing_brs: false,
259         statusbar: false,
260         menubar: false,
261         paste_data_images: false,
262         extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked|style]',
263         automatic_uploads: false,
264         custom_elements: 'doc-root,code-block',
265         valid_children: [
266             "-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]",
267             "+div[pre|img]",
268             "-doc-root[doc-root|#text]",
269             "-li[details]",
270             "+code-block[pre]",
271             "+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div]"
272         ].join(','),
273         plugins: gatherPlugins(options),
274         contextmenu: false,
275         toolbar: getPrimaryToolbar(options),
276         content_style: getContentStyle(options),
277         style_formats,
278         style_formats_merge: false,
279         media_alt_source: false,
280         media_poster: false,
281         formats,
282         table_style_by_css: true,
283         table_use_colgroups: true,
284         file_picker_types: 'file image',
285         color_map,
286         file_picker_callback,
287         paste_preprocess(plugin, args) {
288             const content = args.content;
289             if (content.indexOf('<img src="file://') !== -1) {
290                 args.content = '';
291             }
292         },
293         init_instance_callback(editor) {
294             const head = editor.getDoc().querySelector('head');
295             head.innerHTML += fetchCustomHeadContent();
296         },
297         setup(editor) {
298             registerCustomIcons(editor);
299             registerAdditionalToolbars(editor, options);
300             getSetupCallback(options)(editor);
301         },
302     };
303 }
304
305 /**
306  * @typedef {Object} WysiwygConfigOptions
307  * @property {Element} containerElement
308  * @property {string} language
309  * @property {boolean} darkMode
310  * @property {string} textDirection
311  * @property {string} drawioUrl
312  * @property {int} pageId
313  * @property {Object} translations
314  * @property {Object} translationMap
315  */