]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
Removed forced initial image manager display
[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 styleFormats = [
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     {
22         title: 'Paragraph', format: 'p', exact: true, classes: '',
23     },
24     {title: 'Blockquote', format: 'blockquote'},
25     {
26         title: 'Callouts',
27         items: [
28             {title: 'Information', format: 'calloutinfo'},
29             {title: 'Success', format: 'calloutsuccess'},
30             {title: 'Warning', format: 'calloutwarning'},
31             {title: 'Danger', format: 'calloutdanger'},
32         ],
33     },
34 ];
35
36 const formats = {
37     alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
38     aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
39     alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
40     calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
41     calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
42     calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
43     calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}},
44 };
45
46 const colorMap = [
47     '#BFEDD2', '',
48     '#FBEEB8', '',
49     '#F8CAC6', '',
50     '#ECCAFA', '',
51     '#C2E0F4', '',
52
53     '#2DC26B', '',
54     '#F1C40F', '',
55     '#E03E2D', '',
56     '#B96AD9', '',
57     '#3598DB', '',
58
59     '#169179', '',
60     '#E67E23', '',
61     '#BA372A', '',
62     '#843FA1', '',
63     '#236FA1', '',
64
65     '#ECF0F1', '',
66     '#CED4D9', '',
67     '#95A5A6', '',
68     '#7E8C8D', '',
69     '#34495E', '',
70
71     '#000000', '',
72     '#ffffff', '',
73 ];
74
75 function filePickerCallback(callback, value, meta) {
76     // field_name, url, type, win
77     if (meta.filetype === 'file') {
78         /** @type {EntitySelectorPopup} * */
79         const selector = window.$components.first('entity-selector-popup');
80         selector.show(entity => {
81             callback(entity.link, {
82                 text: entity.name,
83                 title: entity.name,
84             });
85         });
86     }
87
88     if (meta.filetype === 'image') {
89         // Show image manager
90         /** @type {ImageManager} * */
91         const imageManager = window.$components.first('image-manager');
92         imageManager.show(image => {
93             callback(image.url, {alt: image.name});
94         }, 'gallery');
95     }
96 }
97
98 /**
99  * @param {WysiwygConfigOptions} options
100  * @return {string[]}
101  */
102 function gatherPlugins(options) {
103     const plugins = [
104         'image',
105         'table',
106         'link',
107         'autolink',
108         'fullscreen',
109         'code',
110         'customhr',
111         'autosave',
112         'lists',
113         'codeeditor',
114         'media',
115         'imagemanager',
116         'about',
117         'details',
118         'tasklist',
119         options.textDirection === 'rtl' ? 'directionality' : '',
120     ];
121
122     window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin());
123     window.tinymce.PluginManager.add('customhr', getCustomhrPlugin());
124     window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin());
125     window.tinymce.PluginManager.add('about', getAboutPlugin());
126     window.tinymce.PluginManager.add('details', getDetailsPlugin());
127     window.tinymce.PluginManager.add('tasklist', getTasklistPlugin());
128
129     if (options.drawioUrl) {
130         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
131         plugins.push('drawio');
132     }
133
134     return plugins.filter(plugin => Boolean(plugin));
135 }
136
137 /**
138  * Fetch custom HTML head content from the parent page head into the editor.
139  */
140 function fetchCustomHeadContent() {
141     const headContentLines = document.head.innerHTML.split('\n');
142     const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
143     const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
144     if (startLineIndex === -1 || endLineIndex === -1) {
145         return '';
146     }
147     return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
148 }
149
150 /**
151  * Setup a serializer filter for <br> tags to ensure they're not rendered
152  * within code blocks and that we use newlines there instead.
153  * @param {Editor} editor
154  */
155 function setupBrFilter(editor) {
156     editor.serializer.addNodeFilter('br', nodes => {
157         for (const node of nodes) {
158             if (node.parent && node.parent.name === 'code') {
159                 const newline = window.tinymce.html.Node.create('#text');
160                 newline.value = '\n';
161                 node.replace(newline);
162             }
163         }
164     });
165 }
166
167 /**
168  * @param {WysiwygConfigOptions} options
169  * @return {function(Editor)}
170  */
171 function getSetupCallback(options) {
172     return function setupCallback(editor) {
173         function editorChange() {
174             if (options.darkMode) {
175                 editor.contentDocument.documentElement.classList.add('dark-mode');
176             }
177             window.$events.emit('editor-html-change', '');
178         }
179
180         editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
181         listenForCommonEvents(editor);
182         listenForDragAndPaste(editor, options);
183
184         editor.on('init', () => {
185             editorChange();
186             scrollToQueryString(editor);
187             window.editor = editor;
188             registerShortcuts(editor);
189         });
190
191         editor.on('PreInit', () => {
192             setupBrFilter(editor);
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     // 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|hr]',
272         ].join(','),
273         plugins: gatherPlugins(options),
274         contextmenu: false,
275         toolbar: getPrimaryToolbar(options),
276         content_style: getContentStyle(options),
277         style_formats: styleFormats,
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: colorMap,
286         file_picker_callback: filePickerCallback,
287         paste_preprocess(plugin, args) {
288             const {content} = args;
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);
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  */