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