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