]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
Started support for WYSIWYG details/summary blocks
[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 import {getPlugin as getDetailsPlugin} from "./plugins-details";
12
13 const style_formats = [
14     {title: "Large Header", format: "h2", preview: 'color: blue;'},
15     {title: "Medium Header", format: "h3"},
16     {title: "Small Header", format: "h4"},
17     {title: "Tiny Header", format: "h5"},
18     {title: "Paragraph", format: "p", exact: true, classes: ''},
19     {title: "Blockquote", format: "blockquote"},
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     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 details'
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         "details",
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     window.tinymce.PluginManager.add('details', getDetailsPlugin(options));
133
134     if (options.drawioUrl) {
135         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
136         plugins.push('drawio');
137     }
138
139     return plugins.filter(plugin => Boolean(plugin)).join(' ');
140 }
141
142 /**
143  * Fetch custom HTML head content from the parent page head into the editor.
144  */
145 function fetchCustomHeadContent() {
146     const headContentLines = document.head.innerHTML.split("\n");
147     const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
148     const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
149     if (startLineIndex === -1 || endLineIndex === -1) {
150         return ''
151     }
152     return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
153 }
154
155 /**
156  * @param {WysiwygConfigOptions} options
157  * @return {function(Editor)}
158  */
159 function getSetupCallback(options) {
160     return function(editor) {
161         editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
162         listenForCommonEvents(editor);
163         registerShortcuts(editor);
164         listenForDragAndPaste(editor, options);
165
166         editor.on('init', () => {
167             editorChange();
168             scrollToQueryString(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         // Custom handler hook
181         window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
182
183         // Inline code format button
184         editor.ui.registry.addButton('inlinecode', {
185             tooltip: 'Inline code',
186             icon: 'sourcecode',
187             onAction() {
188                 editor.execCommand('mceToggleFormat', false, 'code');
189             }
190         })
191     }
192 }
193
194 /**
195  * @param {WysiwygConfigOptions} options
196  */
197 function getContentStyle(options) {
198     return `
199 html, body, html.dark-mode {
200     background: ${options.darkMode ? '#222' : '#fff'};
201
202 body {
203     padding-left: 15px !important;
204     padding-right: 15px !important; 
205     height: initial !important;
206     margin:0!important; 
207     margin-left: auto! important;
208     margin-right: auto !important;
209     overflow-y: hidden !important;
210 }`.trim().replace('\n', '');
211 }
212
213 /**
214  * @param {WysiwygConfigOptions} options
215  * @return {Object}
216  */
217 export function build(options) {
218
219     // Set language
220     window.tinymce.addI18n(options.language, options.translationMap);
221
222     const {toolbar, groupButtons: toolBarGroupButtons} = buildToolbar(options);
223
224     // Return config object
225     return {
226         width: '100%',
227         height: '100%',
228         selector: '#html-editor',
229         content_css: [
230             window.baseUrl('/dist/styles.css'),
231         ],
232         branding: false,
233         skin: options.darkMode ? 'oxide-dark' : 'oxide',
234         body_class: 'page-content',
235         browser_spellcheck: true,
236         relative_urls: false,
237         language: options.language,
238         directionality: options.textDirection,
239         remove_script_host: false,
240         document_base_url: window.baseUrl('/'),
241         end_container_on_empty_block: true,
242         statusbar: false,
243         menubar: false,
244         paste_data_images: false,
245         extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*]',
246         automatic_uploads: false,
247         valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
248         plugins: gatherPlugins(options),
249         imagetools_toolbar: 'imageoptions',
250         contextmenu: false,
251         toolbar: toolbar,
252         content_style: getContentStyle(options),
253         style_formats,
254         style_formats_merge: false,
255         media_alt_source: false,
256         media_poster: false,
257         formats,
258         file_picker_types: 'file image',
259         file_picker_callback,
260         paste_preprocess(plugin, args) {
261             const content = args.content;
262             if (content.indexOf('<img src="file://') !== -1) {
263                 args.content = '';
264             }
265         },
266         init_instance_callback(editor) {
267             const head = editor.getDoc().querySelector('head');
268             head.innerHTML += fetchCustomHeadContent();
269         },
270         setup(editor) {
271             for (const [key, config] of Object.entries(toolBarGroupButtons)) {
272                 editor.ui.registry.addGroupToolbarButton(key, config);
273             }
274             getSetupCallback(options)(editor);
275         },
276     };
277 }
278
279 /**
280  * @typedef {Object} WysiwygConfigOptions
281  * @property {Element} containerElement
282  * @property {string} language
283  * @property {boolean} darkMode
284  * @property {string} textDirection
285  * @property {string} drawioUrl
286  * @property {int} pageId
287  * @property {Object} translations
288  * @property {Object} translationMap
289  */