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 import {registerCustomIcons} from './icons';
7 import {setupFilters} from './filters';
9 import {getPlugin as getCodeeditorPlugin} from './plugin-codeeditor';
10 import {getPlugin as getDrawioPlugin} from './plugin-drawio';
11 import {getPlugin as getCustomhrPlugin} from './plugins-customhr';
12 import {getPlugin as getImagemanagerPlugin} from './plugins-imagemanager';
13 import {getPlugin as getAboutPlugin} from './plugins-about';
14 import {getPlugin as getDetailsPlugin} from './plugins-details';
15 import {getPlugin as getTableAdditionsPlugin} from './plugins-table-additions';
16 import {getPlugin as getTasklistPlugin} from './plugins-tasklist';
18 handleTableCellRangeEvents,
19 handleEmbedAlignmentChanges,
20 handleTextDirectionCleaning,
23 const styleFormats = [
24 {title: 'Large Header', format: 'h2', preview: 'color: blue;'},
25 {title: 'Medium Header', format: 'h3'},
26 {title: 'Small Header', format: 'h4'},
27 {title: 'Tiny Header', format: 'h5'},
29 title: 'Paragraph', format: 'p', exact: true, classes: '',
31 {title: 'Blockquote', format: 'blockquote'},
35 {title: 'Information', format: 'calloutinfo'},
36 {title: 'Success', format: 'calloutsuccess'},
37 {title: 'Warning', format: 'calloutwarning'},
38 {title: 'Danger', format: 'calloutdanger'},
44 alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video', classes: 'align-left'},
45 aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video', classes: 'align-center'},
46 alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video', classes: 'align-right'},
47 calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
48 calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
49 calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
50 calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}},
82 function filePickerCallback(callback, value, meta) {
83 // field_name, url, type, win
84 if (meta.filetype === 'file') {
85 /** @type {EntitySelectorPopup} * */
86 const selector = window.$components.first('entity-selector-popup');
87 const selectionText = this.selection.getContent({format: 'text'}).trim();
88 selector.show(entity => {
89 callback(entity.link, {
94 initialValue: selectionText,
95 searchEndpoint: '/search/entity-selector',
96 entityTypes: 'page,book,chapter,bookshelf',
97 entityPermission: 'view',
101 if (meta.filetype === 'image') {
102 // Show image manager
103 /** @type {ImageManager} * */
104 const imageManager = window.$components.first('image-manager');
105 imageManager.show(image => {
106 callback(image.url, {alt: image.name});
112 * @param {WysiwygConfigOptions} options
115 function gatherPlugins(options) {
133 options.textDirection === 'rtl' ? 'directionality' : '',
136 window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin());
137 window.tinymce.PluginManager.add('customhr', getCustomhrPlugin());
138 window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin());
139 window.tinymce.PluginManager.add('about', getAboutPlugin());
140 window.tinymce.PluginManager.add('details', getDetailsPlugin());
141 window.tinymce.PluginManager.add('tasklist', getTasklistPlugin());
142 window.tinymce.PluginManager.add('tableadditions', getTableAdditionsPlugin());
144 if (options.drawioUrl) {
145 window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
146 plugins.push('drawio');
149 return plugins.filter(plugin => Boolean(plugin));
153 * Fetch custom HTML head content nodes from the outer page head
154 * and add them to the given editor document.
155 * @param {Document} editorDoc
157 function addCustomHeadContent(editorDoc) {
158 const headContentLines = document.head.innerHTML.split('\n');
159 const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
160 const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
161 if (startLineIndex === -1 || endLineIndex === -1) {
165 const customHeadHtml = headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
166 const el = editorDoc.createElement('div');
167 el.innerHTML = customHeadHtml;
169 editorDoc.head.append(...el.children);
173 * @param {WysiwygConfigOptions} options
174 * @return {function(Editor)}
176 function getSetupCallback(options) {
177 return function setupCallback(editor) {
178 function editorChange() {
179 if (options.darkMode) {
180 editor.contentDocument.documentElement.classList.add('dark-mode');
182 window.$events.emit('editor-html-change', '');
185 editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
186 listenForCommonEvents(editor);
187 listenForDragAndPaste(editor, options);
189 editor.on('init', () => {
191 scrollToQueryString(editor);
192 window.editor = editor;
193 registerShortcuts(editor);
196 editor.on('PreInit', () => {
197 setupFilters(editor);
200 handleEmbedAlignmentChanges(editor);
201 handleTableCellRangeEvents(editor);
202 handleTextDirectionCleaning(editor);
204 // Custom handler hook
205 window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
207 // Inline code format button
208 editor.ui.registry.addButton('inlinecode', {
209 tooltip: 'Inline code',
212 editor.execCommand('mceToggleFormat', false, 'code');
219 * @param {WysiwygConfigOptions} options
221 function getContentStyle(options) {
223 html, body, html.dark-mode {
224 background: ${options.darkMode ? '#222' : '#fff'};
227 padding-left: 15px !important;
228 padding-right: 15px !important;
229 height: initial !important;
231 margin-left: auto! important;
232 margin-right: auto !important;
233 overflow-y: hidden !important;
234 }`.trim().replace('\n', '');
238 * @param {WysiwygConfigOptions} options
241 export function buildForEditor(options) {
243 window.tinymce.addI18n(options.language, options.translationMap);
246 const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
248 // Return config object
252 selector: '#html-editor',
253 cache_suffix: `?version=${version}`,
255 window.baseUrl('/dist/styles.css'),
258 skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
259 body_class: 'page-content',
260 browser_spellcheck: true,
261 relative_urls: false,
262 language: options.language,
263 directionality: options.textDirection,
264 remove_script_host: false,
265 document_base_url: window.baseUrl('/'),
266 end_container_on_empty_block: true,
267 remove_trailing_brs: false,
270 paste_data_images: false,
271 extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked|style]',
272 automatic_uploads: false,
273 custom_elements: 'doc-root,code-block',
275 '-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]',
277 '-doc-root[doc-root|#text]',
280 '+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div|hr]',
282 plugins: gatherPlugins(options),
284 toolbar: getPrimaryToolbar(options),
285 content_style: getContentStyle(options),
286 style_formats: styleFormats,
287 style_formats_merge: false,
288 media_alt_source: false,
291 table_style_by_css: true,
292 table_use_colgroups: true,
293 file_picker_types: 'file image',
295 file_picker_callback: filePickerCallback,
296 paste_preprocess(plugin, args) {
297 const {content} = args;
298 if (content.indexOf('<img src="file://') !== -1) {
302 init_instance_callback(editor) {
303 addCustomHeadContent(editor.getDoc());
306 registerCustomIcons(editor);
307 registerAdditionalToolbars(editor);
308 getSetupCallback(options)(editor);
314 * @param {WysiwygConfigOptions} options
315 * @return {RawEditorOptions}
317 export function buildForInput(options) {
319 window.tinymce.addI18n(options.language, options.translationMap);
322 const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
324 // Return config object
328 target: options.containerElement,
329 cache_suffix: `?version=${version}`,
331 window.baseUrl('/dist/styles.css'),
334 skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
335 body_class: 'wysiwyg-input',
336 browser_spellcheck: true,
337 relative_urls: false,
338 language: options.language,
339 directionality: options.textDirection,
340 remove_script_host: false,
341 document_base_url: window.baseUrl('/'),
342 end_container_on_empty_block: true,
343 remove_trailing_brs: false,
346 plugins: 'link autolink lists',
348 toolbar: 'bold italic link bullist numlist',
349 content_style: getContentStyle(options),
350 file_picker_types: 'file',
351 valid_elements: 'p,a[href|title|target],ol,ul,li,strong,em,br',
352 file_picker_callback: filePickerCallback,
353 init_instance_callback(editor) {
354 addCustomHeadContent(editor.getDoc());
356 editor.contentDocument.documentElement.classList.toggle('dark-mode', options.darkMode);
362 * @typedef {Object} WysiwygConfigOptions
363 * @property {Element} containerElement
364 * @property {string} language
365 * @property {boolean} darkMode
366 * @property {string} textDirection
367 * @property {string} drawioUrl
368 * @property {int} pageId
369 * @property {Object} translations
370 * @property {Object} translationMap