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