]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
Skip intermediate login page with single provider
[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 import {getPrimaryToolbar, registerAdditionalToolbars} from "./toolbars";
6
7 import {getPlugin as getCodeeditorPlugin} from "./plugin-codeeditor";
8 import {getPlugin as getDrawioPlugin} from "./plugin-drawio";
9 import {getPlugin as getCustomhrPlugin} from "./plugins-customhr";
10 import {getPlugin as getImagemanagerPlugin} from "./plugins-imagemanager";
11 import {getPlugin as getAboutPlugin} from "./plugins-about";
12 import {getPlugin as getDetailsPlugin} from "./plugins-details";
13 import {getPlugin as getTasklistPlugin} from "./plugins-tasklist";
14
15 const style_formats = [
16     {title: "Large Header", format: "h2", preview: 'color: blue;'},
17     {title: "Medium Header", format: "h3"},
18     {title: "Small Header", format: "h4"},
19     {title: "Tiny Header", format: "h5"},
20     {title: "Paragraph", format: "p", exact: true, classes: ''},
21     {title: "Blockquote", format: "blockquote"},
22     {
23         title: "Callouts", items: [
24             {title: "Information", format: 'calloutinfo'},
25             {title: "Success", format: 'calloutsuccess'},
26             {title: "Warning", format: 'calloutwarning'},
27             {title: "Danger", format: 'calloutdanger'}
28         ]
29     },
30 ];
31
32 const formats = {
33     alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
34     aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
35     alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
36     calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
37     calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
38     calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
39     calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
40 };
41
42 function file_picker_callback(callback, value, meta) {
43
44     // field_name, url, type, win
45     if (meta.filetype === 'file') {
46         window.EntitySelectorPopup.show(entity => {
47             callback(entity.link, {
48                 text: entity.name,
49                 title: entity.name,
50             });
51         });
52     }
53
54     if (meta.filetype === 'image') {
55         // Show image manager
56         window.ImageManager.show(function (image) {
57             callback(image.url, {alt: image.name});
58         }, 'gallery');
59     }
60
61 }
62
63 /**
64  * @param {WysiwygConfigOptions} options
65  * @return {string}
66  */
67 function gatherPlugins(options) {
68     const plugins = [
69         "image",
70         "imagetools",
71         "table",
72         "paste",
73         "link",
74         "autolink",
75         "fullscreen",
76         "code",
77         "customhr",
78         "autosave",
79         "lists",
80         "codeeditor",
81         "media",
82         "imagemanager",
83         "about",
84         "details",
85         "tasklist",
86         options.textDirection === 'rtl' ? 'directionality' : '',
87     ];
88
89     window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin(options));
90     window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
91     window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
92     window.tinymce.PluginManager.add('about', getAboutPlugin(options));
93     window.tinymce.PluginManager.add('details', getDetailsPlugin(options));
94     window.tinymce.PluginManager.add('tasklist', getTasklistPlugin(options));
95
96     if (options.drawioUrl) {
97         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
98         plugins.push('drawio');
99     }
100
101     return plugins.filter(plugin => Boolean(plugin)).join(' ');
102 }
103
104 /**
105  * Fetch custom HTML head content from the parent page head into the editor.
106  */
107 function fetchCustomHeadContent() {
108     const headContentLines = document.head.innerHTML.split("\n");
109     const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
110     const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
111     if (startLineIndex === -1 || endLineIndex === -1) {
112         return ''
113     }
114     return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
115 }
116
117 /**
118  * Setup a serializer filter for <br> tags to ensure they're not rendered
119  * within code blocks and that we use newlines there instead.
120  * @param {Editor} editor
121  */
122 function setupBrFilter(editor) {
123     editor.serializer.addNodeFilter('br', function(nodes) {
124         for (const node of nodes) {
125             if (node.parent && node.parent.name === 'code') {
126                 const newline = new tinymce.html.Node.create('#text');
127                 newline.value = '\n';
128                 node.replace(newline);
129             }
130         }
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             window.editor = editor;
149         });
150
151         editor.on('PreInit', () => {
152             setupBrFilter(editor);
153         });
154
155         function editorChange() {
156             const content = editor.getContent();
157             if (options.darkMode) {
158                 editor.contentDocument.documentElement.classList.add('dark-mode');
159             }
160             window.$events.emit('editor-html-change', content);
161         }
162
163         // Custom handler hook
164         window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
165
166         // Inline code format button
167         editor.ui.registry.addButton('inlinecode', {
168             tooltip: 'Inline code',
169             icon: 'sourcecode',
170             onAction() {
171                 editor.execCommand('mceToggleFormat', false, 'code');
172             }
173         })
174     }
175 }
176
177 /**
178  * @param {WysiwygConfigOptions} options
179  */
180 function getContentStyle(options) {
181     return `
182 html, body, html.dark-mode {
183     background: ${options.darkMode ? '#222' : '#fff'};
184
185 body {
186     padding-left: 15px !important;
187     padding-right: 15px !important; 
188     height: initial !important;
189     margin:0!important; 
190     margin-left: auto! important;
191     margin-right: auto !important;
192     overflow-y: hidden !important;
193 }`.trim().replace('\n', '');
194 }
195
196 /**
197  * @param {WysiwygConfigOptions} options
198  * @return {Object}
199  */
200 export function build(options) {
201
202     // Set language
203     window.tinymce.addI18n(options.language, options.translationMap);
204
205     // BookStack Version
206     const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
207
208     // Return config object
209     return {
210         width: '100%',
211         height: '100%',
212         selector: '#html-editor',
213         cache_suffix: '?version=' + version,
214         content_css: [
215             window.baseUrl('/dist/styles.css'),
216         ],
217         branding: false,
218         skin: options.darkMode ? 'oxide-dark' : 'oxide',
219         body_class: 'page-content',
220         browser_spellcheck: true,
221         relative_urls: false,
222         language: options.language,
223         directionality: options.textDirection,
224         remove_script_host: false,
225         document_base_url: window.baseUrl('/'),
226         end_container_on_empty_block: true,
227         remove_trailing_brs: false,
228         statusbar: false,
229         menubar: false,
230         paste_data_images: false,
231         extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked]',
232         automatic_uploads: false,
233         custom_elements: 'doc-root,code-block',
234         valid_children: [
235             "-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]",
236             "+div[pre|img]",
237             "-doc-root[doc-root|#text]",
238             "-li[details]",
239             "+code-block[pre]",
240             "+doc-root[code-block]"
241         ].join(','),
242         plugins: gatherPlugins(options),
243         imagetools_toolbar: 'imageoptions',
244         contextmenu: false,
245         toolbar: getPrimaryToolbar(options),
246         content_style: getContentStyle(options),
247         style_formats,
248         style_formats_merge: false,
249         media_alt_source: false,
250         media_poster: false,
251         formats,
252         file_picker_types: 'file image',
253         file_picker_callback,
254         paste_preprocess(plugin, args) {
255             const content = args.content;
256             if (content.indexOf('<img src="file://') !== -1) {
257                 args.content = '';
258             }
259         },
260         init_instance_callback(editor) {
261             const head = editor.getDoc().querySelector('head');
262             head.innerHTML += fetchCustomHeadContent();
263         },
264         setup(editor) {
265             registerAdditionalToolbars(editor, options);
266             getSetupCallback(options)(editor);
267         },
268     };
269 }
270
271 /**
272  * @typedef {Object} WysiwygConfigOptions
273  * @property {Element} containerElement
274  * @property {string} language
275  * @property {boolean} darkMode
276  * @property {string} textDirection
277  * @property {string} drawioUrl
278  * @property {int} pageId
279  * @property {Object} translations
280  * @property {Object} translationMap
281  */
Morty Proxy This is a proxified and sanitized view of the page, visit original site.