]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
New Crowdin updates (#3312)
[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         remove_trailing_brs: false,
247         statusbar: false,
248         menubar: false,
249         paste_data_images: false,
250         extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*]',
251         automatic_uploads: false,
252         custom_elements: 'doc-root,code-block',
253         valid_children: [
254             "-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]",
255             "+div[pre|img]",
256             "-doc-root[doc-root|#text]",
257             "-li[details]",
258             "+code-block[pre]",
259             "+doc-root[code-block]"
260         ].join(','),
261         plugins: gatherPlugins(options),
262         imagetools_toolbar: 'imageoptions',
263         contextmenu: false,
264         toolbar: toolbar,
265         content_style: getContentStyle(options),
266         style_formats,
267         style_formats_merge: false,
268         media_alt_source: false,
269         media_poster: false,
270         formats,
271         file_picker_types: 'file image',
272         file_picker_callback,
273         paste_preprocess(plugin, args) {
274             const content = args.content;
275             if (content.indexOf('<img src="file://') !== -1) {
276                 args.content = '';
277             }
278         },
279         init_instance_callback(editor) {
280             const head = editor.getDoc().querySelector('head');
281             head.innerHTML += fetchCustomHeadContent();
282         },
283         setup(editor) {
284             for (const [key, config] of Object.entries(toolBarGroupButtons)) {
285                 editor.ui.registry.addGroupToolbarButton(key, config);
286             }
287             getSetupCallback(options)(editor);
288         },
289     };
290 }
291
292 /**
293  * @typedef {Object} WysiwygConfigOptions
294  * @property {Element} containerElement
295  * @property {string} language
296  * @property {boolean} darkMode
297  * @property {string} textDirection
298  * @property {string} drawioUrl
299  * @property {int} pageId
300  * @property {Object} translations
301  * @property {Object} translationMap
302  */