]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/config.js
Started initial tasklist attempt, failed implementation
[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  * @param {WysiwygConfigOptions} options
119  * @return {function(Editor)}
120  */
121 function getSetupCallback(options) {
122     return function(editor) {
123         editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
124         listenForCommonEvents(editor);
125         registerShortcuts(editor);
126         listenForDragAndPaste(editor, options);
127
128         editor.on('init', () => {
129             editorChange();
130             scrollToQueryString(editor);
131             window.editor = editor;
132         });
133
134         function editorChange() {
135             const content = editor.getContent();
136             if (options.darkMode) {
137                 editor.contentDocument.documentElement.classList.add('dark-mode');
138             }
139             window.$events.emit('editor-html-change', content);
140         }
141
142         // Custom handler hook
143         window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
144
145         // Inline code format button
146         editor.ui.registry.addButton('inlinecode', {
147             tooltip: 'Inline code',
148             icon: 'sourcecode',
149             onAction() {
150                 editor.execCommand('mceToggleFormat', false, 'code');
151             }
152         })
153     }
154 }
155
156 /**
157  * @param {WysiwygConfigOptions} options
158  */
159 function getContentStyle(options) {
160     return `
161 html, body, html.dark-mode {
162     background: ${options.darkMode ? '#222' : '#fff'};
163
164 body {
165     padding-left: 15px !important;
166     padding-right: 15px !important; 
167     height: initial !important;
168     margin:0!important; 
169     margin-left: auto! important;
170     margin-right: auto !important;
171     overflow-y: hidden !important;
172 }`.trim().replace('\n', '');
173 }
174
175 /**
176  * @param {WysiwygConfigOptions} options
177  * @return {Object}
178  */
179 export function build(options) {
180
181     // Set language
182     window.tinymce.addI18n(options.language, options.translationMap);
183
184     // BookStack Version
185     const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
186
187     // Return config object
188     return {
189         width: '100%',
190         height: '100%',
191         selector: '#html-editor',
192         cache_suffix: '?version=' + version,
193         content_css: [
194             window.baseUrl('/dist/styles.css'),
195         ],
196         branding: false,
197         skin: options.darkMode ? 'oxide-dark' : 'oxide',
198         body_class: 'page-content',
199         browser_spellcheck: true,
200         relative_urls: false,
201         language: options.language,
202         directionality: options.textDirection,
203         remove_script_host: false,
204         document_base_url: window.baseUrl('/'),
205         end_container_on_empty_block: true,
206         remove_trailing_brs: false,
207         statusbar: false,
208         menubar: false,
209         paste_data_images: false,
210         extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class]',
211         automatic_uploads: false,
212         custom_elements: 'doc-root,code-block',
213         valid_children: [
214             "-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]",
215             "+div[pre|img]",
216             "-doc-root[doc-root|#text]",
217             "-li[details]",
218             "+code-block[pre]",
219             "+doc-root[code-block]"
220         ].join(','),
221         plugins: gatherPlugins(options),
222         imagetools_toolbar: 'imageoptions',
223         contextmenu: false,
224         toolbar: getPrimaryToolbar(options),
225         content_style: getContentStyle(options),
226         style_formats,
227         style_formats_merge: false,
228         media_alt_source: false,
229         media_poster: false,
230         formats,
231         file_picker_types: 'file image',
232         file_picker_callback,
233         paste_preprocess(plugin, args) {
234             const content = args.content;
235             if (content.indexOf('<img src="file://') !== -1) {
236                 args.content = '';
237             }
238         },
239         init_instance_callback(editor) {
240             const head = editor.getDoc().querySelector('head');
241             head.innerHTML += fetchCustomHeadContent();
242         },
243         setup(editor) {
244             registerAdditionalToolbars(editor, options);
245             getSetupCallback(options)(editor);
246         },
247     };
248 }
249
250 /**
251  * @typedef {Object} WysiwygConfigOptions
252  * @property {Element} containerElement
253  * @property {string} language
254  * @property {boolean} darkMode
255  * @property {string} textDirection
256  * @property {string} drawioUrl
257  * @property {int} pageId
258  * @property {Object} translations
259  * @property {Object} translationMap
260  */