]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
Added help/about box to wysiwyg editor
[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     {title: "Inline Code", inline: "code"},
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     codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
32     alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
33     aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
34     alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
35     calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
36     calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
37     calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
38     calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
39 };
40
41 function file_picker_callback(callback, value, meta) {
42
43     // field_name, url, type, win
44     if (meta.filetype === 'file') {
45         window.EntitySelectorPopup.show(entity => {
46             callback(entity.link, {
47                 text: entity.name,
48                 title: entity.name,
49             });
50         });
51     }
52
53     if (meta.filetype === 'image') {
54         // Show image manager
55         window.ImageManager.show(function (image) {
56             callback(image.url, {alt: image.name});
57         }, 'gallery');
58     }
59
60 }
61
62 /**
63  * @param {WysiwygConfigOptions} options
64  * @return {string}
65  */
66 function buildToolbar(options) {
67     const textDirPlugins = options.textDirection === 'rtl' ? 'ltr rtl' : '';
68
69     const toolbar = [
70         'undo redo',
71         'styleselect',
72         'bold italic underline strikethrough superscript subscript',
73         'forecolor backcolor',
74         'alignleft aligncenter alignright alignjustify',
75         'bullist numlist outdent indent',
76         textDirPlugins,
77         'table imagemanager-insert link hr codeeditor drawio media',
78         'removeformat code about fullscreen'
79     ];
80
81     return toolbar.filter(row => Boolean(row)).join(' | ');
82 }
83
84 /**
85  * @param {WysiwygConfigOptions} options
86  * @return {string}
87  */
88 function gatherPlugins(options) {
89     const plugins = [
90         "image",
91         "imagetools",
92         "table",
93         "paste",
94         "link",
95         "autolink",
96         "fullscreen",
97         "code",
98         "customhr",
99         "autosave",
100         "lists",
101         "codeeditor",
102         "media",
103         "imagemanager",
104         "about",
105         options.textDirection === 'rtl' ? 'directionality' : '',
106     ];
107
108     window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin(options));
109     window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
110     window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
111     window.tinymce.PluginManager.add('about', getAboutPlugin(options));
112
113     if (options.drawioUrl) {
114         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
115         plugins.push('drawio');
116     }
117
118     return plugins.filter(plugin => Boolean(plugin)).join(' ');
119 }
120
121 /**
122  * Load custom HTML head content from the settings into the editor.
123  * TODO: We should be able to get this from current parent page?
124  * @param {Editor} editor
125  */
126 function loadCustomHeadContent(editor) {
127     window.$http.get(window.baseUrl('/custom-head-content')).then(resp => {
128         if (!resp.data) return;
129         let head = editor.getDoc().querySelector('head');
130         head.innerHTML += resp.data;
131     });
132 }
133
134 /**
135  * @param {WysiwygConfigOptions} options
136  * @return {function(Editor)}
137  */
138 function getSetupCallback(options) {
139     return function(editor) {
140         editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
141         listenForCommonEvents(editor);
142         registerShortcuts(editor);
143         listenForDragAndPaste(editor, options);
144
145         editor.on('init', () => {
146             editorChange();
147             scrollToQueryString(editor);
148             fixScrollForMobile(editor);
149             window.editor = editor;
150         });
151
152         function editorChange() {
153             const content = editor.getContent();
154             if (options.darkMode) {
155                 editor.contentDocument.documentElement.classList.add('dark-mode');
156             }
157             window.$events.emit('editor-html-change', content);
158         }
159
160         // TODO - Update to standardise across both editors
161         // Use events within listenForBookStackEditorEvents instead (Different event signature)
162         window.$events.listen('editor-html-update', html => {
163             editor.setContent(html);
164             editor.selection.select(editor.getBody(), true);
165             editor.selection.collapse(false);
166             editorChange(html);
167         });
168
169         // Custom handler hook
170         window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
171     }
172 }
173
174 /**
175  * @param {WysiwygConfigOptions} options
176  * @return {Object}
177  */
178 export function build(options) {
179
180     // Set language
181     window.tinymce.addI18n(options.language, options.translationMap);
182
183     // Return config object
184     return {
185         width: '100%',
186         height: '100%',
187         selector: '#html-editor',
188         content_css: [
189             window.baseUrl('/dist/styles.css'),
190         ],
191         branding: false,
192         skin: options.darkMode ? 'oxide-dark' : 'oxide',
193         body_class: 'page-content',
194         browser_spellcheck: true,
195         relative_urls: false,
196         language: options.language,
197         directionality: options.textDirection,
198         remove_script_host: false,
199         document_base_url: window.baseUrl('/'),
200         end_container_on_empty_block: true,
201         statusbar: false,
202         menubar: false,
203         paste_data_images: false,
204         extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
205         automatic_uploads: false,
206         valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
207         plugins: gatherPlugins(options),
208         imagetools_toolbar: 'imageoptions',
209         contextmenu: false,
210         toolbar: buildToolbar(options),
211         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;}`,
212         style_formats,
213         style_formats_merge: false,
214         media_alt_source: false,
215         media_poster: false,
216         formats,
217         file_picker_types: 'file image',
218         file_picker_callback,
219         paste_preprocess(plugin, args) {
220             let content = args.content;
221             if (content.indexOf('<img src="file://') !== -1) {
222                 args.content = '';
223             }
224         },
225         init_instance_callback(editor) {
226             loadCustomHeadContent(editor);
227         },
228         setup: getSetupCallback(options),
229     };
230 }
231
232 /**
233  * @typedef {Object} WysiwygConfigOptions
234  * @property {Element} containerElement
235  * @property {string} language
236  * @property {boolean} darkMode
237  * @property {string} textDirection
238  * @property {string} drawioUrl
239  * @property {int} pageId
240  * @property {Object} translations
241  * @property {Object} translationMap
242  */