]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
Merge branch 'tinymce' into development
[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
12 const style_formats = [
13     {title: "Large Header", format: "h2", preview: 'color: blue;'},
14     {title: "Medium Header", format: "h3"},
15     {title: "Small Header", format: "h4"},
16     {title: "Tiny Header", format: "h5"},
17     {title: "Paragraph", format: "p", exact: true, classes: ''},
18     {title: "Blockquote", format: "blockquote"},
19     {
20         title: "Callouts", items: [
21             {title: "Information", format: 'calloutinfo'},
22             {title: "Success", format: 'calloutsuccess'},
23             {title: "Warning", format: 'calloutwarning'},
24             {title: "Danger", format: 'calloutdanger'}
25         ]
26     },
27 ];
28
29 const formats = {
30     codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
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'
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         options.textDirection === 'rtl' ? 'directionality' : '',
125     ];
126
127     window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin(options));
128     window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
129     window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
130     window.tinymce.PluginManager.add('about', getAboutPlugin(options));
131
132     if (options.drawioUrl) {
133         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
134         plugins.push('drawio');
135     }
136
137     return plugins.filter(plugin => Boolean(plugin)).join(' ');
138 }
139
140 /**
141  * Fetch custom HTML head content from the parent page head into the editor.
142  */
143 function fetchCustomHeadContent() {
144     const headContentLines = document.head.innerHTML.split("\n");
145     const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
146     const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
147     if (startLineIndex === -1 || endLineIndex === -1) {
148         return ''
149     }
150     return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
151 }
152
153 /**
154  * @param {WysiwygConfigOptions} options
155  * @return {function(Editor)}
156  */
157 function getSetupCallback(options) {
158     return function(editor) {
159         editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
160         listenForCommonEvents(editor);
161         registerShortcuts(editor);
162         listenForDragAndPaste(editor, options);
163
164         editor.on('init', () => {
165             editorChange();
166             scrollToQueryString(editor);
167             window.editor = editor;
168         });
169
170         function editorChange() {
171             const content = editor.getContent();
172             if (options.darkMode) {
173                 editor.contentDocument.documentElement.classList.add('dark-mode');
174             }
175             window.$events.emit('editor-html-change', content);
176         }
177
178         // Custom handler hook
179         window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
180
181         // Inline code format button
182         editor.ui.registry.addButton('inlinecode', {
183             tooltip: 'Inline code',
184             icon: 'sourcecode',
185             onAction() {
186                 editor.execCommand('mceToggleFormat', false, 'code');
187             }
188         })
189     }
190 }
191
192 /**
193  * @param {WysiwygConfigOptions} options
194  * @return {Object}
195  */
196 export function build(options) {
197
198     // Set language
199     window.tinymce.addI18n(options.language, options.translationMap);
200
201     const {toolbar, groupButtons: toolBarGroupButtons} = buildToolbar(options);
202
203     // Return config object
204     return {
205         width: '100%',
206         height: '100%',
207         selector: '#html-editor',
208         content_css: [
209             window.baseUrl('/dist/styles.css'),
210         ],
211         branding: false,
212         skin: options.darkMode ? 'oxide-dark' : 'oxide',
213         body_class: 'page-content',
214         browser_spellcheck: true,
215         relative_urls: false,
216         language: options.language,
217         directionality: options.textDirection,
218         remove_script_host: false,
219         document_base_url: window.baseUrl('/'),
220         end_container_on_empty_block: true,
221         statusbar: false,
222         menubar: false,
223         paste_data_images: false,
224         extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
225         automatic_uploads: false,
226         valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
227         plugins: gatherPlugins(options),
228         imagetools_toolbar: 'imageoptions',
229         contextmenu: false,
230         toolbar: toolbar,
231         content_style: `html, body, html.dark-mode {background: ${options.darkMode ? '#222' : '#fff'};} body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}`,
232         style_formats,
233         style_formats_merge: false,
234         media_alt_source: false,
235         media_poster: false,
236         formats,
237         file_picker_types: 'file image',
238         file_picker_callback,
239         paste_preprocess(plugin, args) {
240             const content = args.content;
241             if (content.indexOf('<img src="file://') !== -1) {
242                 args.content = '';
243             }
244         },
245         init_instance_callback(editor) {
246             const head = editor.getDoc().querySelector('head');
247             head.innerHTML += fetchCustomHeadContent();
248         },
249         setup(editor) {
250             for (const [key, config] of Object.entries(toolBarGroupButtons)) {
251                 editor.ui.registry.addGroupToolbarButton(key, config);
252             }
253             getSetupCallback(options)(editor);
254         },
255     };
256 }
257
258 /**
259  * @typedef {Object} WysiwygConfigOptions
260  * @property {Element} containerElement
261  * @property {string} language
262  * @property {boolean} darkMode
263  * @property {string} textDirection
264  * @property {string} drawioUrl
265  * @property {int} pageId
266  * @property {Object} translations
267  * @property {Object} translationMap
268  */