]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
Added license references to readme attribution
[bookstack] / resources / js / wysiwyg / config.js
1 import {register as registerShortcuts} from "./shortcuts";
2 import {listen as listenForCommonEvents} from "./common-events";
3 import {scrollToQueryString, fixScrollForMobile} 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 formatoverflow',
90         'forecolor backcolor',
91         'alignleft aligncenter alignright alignjustify',
92         'bullist numlist listoverflow',
93         textDirPlugins,
94         'link table imagemanager-insert insertoverflow',
95         'code about fullscreen'
96     ];
97
98     return {
99         toolbar: toolbar.filter(row => Boolean(row)).join(' | '),
100         groupButtons,
101     };
102 }
103
104 /**
105  * @param {WysiwygConfigOptions} options
106  * @return {string}
107  */
108 function gatherPlugins(options) {
109     const plugins = [
110         "image",
111         "imagetools",
112         "table",
113         "paste",
114         "link",
115         "autolink",
116         "fullscreen",
117         "code",
118         "customhr",
119         "autosave",
120         "lists",
121         "codeeditor",
122         "media",
123         "imagemanager",
124         "about",
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
133     if (options.drawioUrl) {
134         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
135         plugins.push('drawio');
136     }
137
138     return plugins.filter(plugin => Boolean(plugin)).join(' ');
139 }
140
141 /**
142  * Load custom HTML head content from the settings into the editor.
143  * TODO: We should be able to get this from current parent page?
144  * @param {Editor} editor
145  */
146 function loadCustomHeadContent(editor) {
147     window.$http.get(window.baseUrl('/custom-head-content')).then(resp => {
148         if (!resp.data) return;
149         let head = editor.getDoc().querySelector('head');
150         head.innerHTML += resp.data;
151     });
152 }
153
154 /**
155  * @param {WysiwygConfigOptions} options
156  * @return {function(Editor)}
157  */
158 function getSetupCallback(options) {
159     return function(editor) {
160         editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
161         listenForCommonEvents(editor);
162         registerShortcuts(editor);
163         listenForDragAndPaste(editor, options);
164
165         editor.on('init', () => {
166             editorChange();
167             scrollToQueryString(editor);
168             fixScrollForMobile(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         // TODO - Update to standardise across both editors
181         // Use events within listenForBookStackEditorEvents instead (Different event signature)
182         window.$events.listen('editor-html-update', html => {
183             editor.setContent(html);
184             editor.selection.select(editor.getBody(), true);
185             editor.selection.collapse(false);
186             editorChange(html);
187         });
188
189         // Custom handler hook
190         window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
191
192         // Inline code format button
193         editor.ui.registry.addButton('inlinecode', {
194             tooltip: 'Inline code',
195             icon: 'sourcecode',
196             onAction() {
197                 editor.execCommand('mceToggleFormat', false, 'code');
198             }
199         })
200     }
201 }
202
203 /**
204  * @param {WysiwygConfigOptions} options
205  * @return {Object}
206  */
207 export function build(options) {
208
209     // Set language
210     window.tinymce.addI18n(options.language, options.translationMap);
211
212     const {toolbar, groupButtons: toolBarGroupButtons} = buildToolbar(options);
213
214     // Return config object
215     return {
216         width: '100%',
217         height: '100%',
218         selector: '#html-editor',
219         content_css: [
220             window.baseUrl('/dist/styles.css'),
221         ],
222         branding: false,
223         skin: options.darkMode ? 'oxide-dark' : 'oxide',
224         body_class: 'page-content',
225         browser_spellcheck: true,
226         relative_urls: false,
227         language: options.language,
228         directionality: options.textDirection,
229         remove_script_host: false,
230         document_base_url: window.baseUrl('/'),
231         end_container_on_empty_block: true,
232         statusbar: false,
233         menubar: false,
234         paste_data_images: false,
235         extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
236         automatic_uploads: false,
237         valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
238         plugins: gatherPlugins(options),
239         imagetools_toolbar: 'imageoptions',
240         contextmenu: false,
241         toolbar: toolbar,
242         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;}`,
243         style_formats,
244         style_formats_merge: false,
245         media_alt_source: false,
246         media_poster: false,
247         formats,
248         file_picker_types: 'file image',
249         file_picker_callback,
250         paste_preprocess(plugin, args) {
251             let content = args.content;
252             if (content.indexOf('<img src="file://') !== -1) {
253                 args.content = '';
254             }
255         },
256         init_instance_callback(editor) {
257             loadCustomHeadContent(editor);
258         },
259         setup(editor) {
260             for (const [key, config] of Object.entries(toolBarGroupButtons)) {
261                 editor.ui.registry.addGroupToolbarButton(key, config);
262             }
263             getSetupCallback(options)(editor);
264         },
265     };
266 }
267
268 /**
269  * @typedef {Object} WysiwygConfigOptions
270  * @property {Element} containerElement
271  * @property {string} language
272  * @property {boolean} darkMode
273  * @property {string} textDirection
274  * @property {string} drawioUrl
275  * @property {int} pageId
276  * @property {Object} translations
277  * @property {Object} translationMap
278  */