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