]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
Revamped workings of WYSIWYG code 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     // Build toolbar content
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[*],div[*]',
246         automatic_uploads: false,
247         custom_elements: 'doc-root,code-block',
248         valid_children: [
249             "-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]",
250             "+div[pre|img]",
251             "-doc-root[doc-root|#text]",
252             "-li[details]",
253             "+code-block[pre]",
254             "+doc-root[code-block]"
255         ].join(','),
256         plugins: gatherPlugins(options),
257         imagetools_toolbar: 'imageoptions',
258         contextmenu: false,
259         toolbar: toolbar,
260         content_style: getContentStyle(options),
261         style_formats,
262         style_formats_merge: false,
263         media_alt_source: false,
264         media_poster: false,
265         formats,
266         file_picker_types: 'file image',
267         file_picker_callback,
268         paste_preprocess(plugin, args) {
269             const content = args.content;
270             if (content.indexOf('<img src="file://') !== -1) {
271                 args.content = '';
272             }
273         },
274         init_instance_callback(editor) {
275             const head = editor.getDoc().querySelector('head');
276             head.innerHTML += fetchCustomHeadContent();
277         },
278         setup(editor) {
279             for (const [key, config] of Object.entries(toolBarGroupButtons)) {
280                 editor.ui.registry.addGroupToolbarButton(key, config);
281             }
282             getSetupCallback(options)(editor);
283         },
284     };
285 }
286
287 /**
288  * @typedef {Object} WysiwygConfigOptions
289  * @property {Element} containerElement
290  * @property {string} language
291  * @property {boolean} darkMode
292  * @property {string} textDirection
293  * @property {string} drawioUrl
294  * @property {int} pageId
295  * @property {Object} translations
296  * @property {Object} translationMap
297  */
Morty Proxy This is a proxified and sanitized view of the page, visit original site.