]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
Changed editor bottom padding technique
[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  */
195 function getContentStyle(options) {
196     return `
197 html, body, html.dark-mode {
198     background: ${options.darkMode ? '#222' : '#fff'};
199
200 body {
201     padding-left: 15px !important;
202     padding-right: 15px !important; 
203     height: initial !important;
204     margin:0!important; 
205     margin-left: auto! important;
206     margin-right: auto !important;
207     overflow-y: hidden !important;
208 }`.trim().replace('\n', '');
209 }
210
211 /**
212  * @param {WysiwygConfigOptions} options
213  * @return {Object}
214  */
215 export function build(options) {
216
217     // Set language
218     window.tinymce.addI18n(options.language, options.translationMap);
219
220     const {toolbar, groupButtons: toolBarGroupButtons} = buildToolbar(options);
221
222     // Return config object
223     return {
224         width: '100%',
225         height: '100%',
226         selector: '#html-editor',
227         content_css: [
228             window.baseUrl('/dist/styles.css'),
229         ],
230         branding: false,
231         skin: options.darkMode ? 'oxide-dark' : 'oxide',
232         body_class: 'page-content',
233         browser_spellcheck: true,
234         relative_urls: false,
235         language: options.language,
236         directionality: options.textDirection,
237         remove_script_host: false,
238         document_base_url: window.baseUrl('/'),
239         end_container_on_empty_block: true,
240         statusbar: false,
241         menubar: false,
242         paste_data_images: false,
243         extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
244         automatic_uploads: false,
245         valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
246         plugins: gatherPlugins(options),
247         imagetools_toolbar: 'imageoptions',
248         contextmenu: false,
249         toolbar: toolbar,
250         content_style: getContentStyle(options),
251         style_formats,
252         style_formats_merge: false,
253         media_alt_source: false,
254         media_poster: false,
255         formats,
256         file_picker_types: 'file image',
257         file_picker_callback,
258         paste_preprocess(plugin, args) {
259             const content = args.content;
260             if (content.indexOf('<img src="file://') !== -1) {
261                 args.content = '';
262             }
263         },
264         init_instance_callback(editor) {
265             const head = editor.getDoc().querySelector('head');
266             head.innerHTML += fetchCustomHeadContent();
267         },
268         setup(editor) {
269             for (const [key, config] of Object.entries(toolBarGroupButtons)) {
270                 editor.ui.registry.addGroupToolbarButton(key, config);
271             }
272             getSetupCallback(options)(editor);
273         },
274     };
275 }
276
277 /**
278  * @typedef {Object} WysiwygConfigOptions
279  * @property {Element} containerElement
280  * @property {string} language
281  * @property {boolean} darkMode
282  * @property {string} textDirection
283  * @property {string} drawioUrl
284  * @property {int} pageId
285  * @property {Object} translations
286  * @property {Object} translationMap
287  */