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 getTasklistPlugin} from './plugins-tasklist';
16 import {handleEmbedAlignmentChanges} from './fixes';
18 const styleFormats = [
19 {title: 'Large Header', format: 'h2', preview: 'color: blue;'},
20 {title: 'Medium Header', format: 'h3'},
21 {title: 'Small Header', format: 'h4'},
22 {title: 'Tiny Header', format: 'h5'},
24 title: 'Paragraph', format: 'p', exact: true, classes: '',
26 {title: 'Blockquote', format: 'blockquote'},
30 {title: 'Information', format: 'calloutinfo'},
31 {title: 'Success', format: 'calloutsuccess'},
32 {title: 'Warning', format: 'calloutwarning'},
33 {title: 'Danger', format: 'calloutdanger'},
39 alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-left'},
40 aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-center'},
41 alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-right'},
42 calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
43 calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
44 calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
45 calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}},
77 function filePickerCallback(callback, value, meta) {
78 // field_name, url, type, win
79 if (meta.filetype === 'file') {
80 /** @type {EntitySelectorPopup} * */
81 const selector = window.$components.first('entity-selector-popup');
82 const selectionText = this.selection.getContent({format: 'text'}).trim();
83 selector.show(entity => {
84 callback(entity.link, {
89 initialValue: selectionText,
90 searchEndpoint: '/search/entity-selector',
91 entityTypes: 'page,book,chapter,bookshelf',
92 entityPermission: 'view',
96 if (meta.filetype === 'image') {
98 /** @type {ImageManager} * */
99 const imageManager = window.$components.first('image-manager');
100 imageManager.show(image => {
101 callback(image.url, {alt: image.name});
107 * @param {WysiwygConfigOptions} options
110 function gatherPlugins(options) {
127 options.textDirection === 'rtl' ? 'directionality' : '',
130 window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin());
131 window.tinymce.PluginManager.add('customhr', getCustomhrPlugin());
132 window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin());
133 window.tinymce.PluginManager.add('about', getAboutPlugin());
134 window.tinymce.PluginManager.add('details', getDetailsPlugin());
135 window.tinymce.PluginManager.add('tasklist', getTasklistPlugin());
137 if (options.drawioUrl) {
138 window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
139 plugins.push('drawio');
142 return plugins.filter(plugin => Boolean(plugin));
146 * Fetch custom HTML head content from the parent page head into the editor.
148 function fetchCustomHeadContent() {
149 const headContentLines = document.head.innerHTML.split('\n');
150 const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
151 const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
152 if (startLineIndex === -1 || endLineIndex === -1) {
155 return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
159 * @param {WysiwygConfigOptions} options
160 * @return {function(Editor)}
162 function getSetupCallback(options) {
163 return function setupCallback(editor) {
164 function editorChange() {
165 if (options.darkMode) {
166 editor.contentDocument.documentElement.classList.add('dark-mode');
168 window.$events.emit('editor-html-change', '');
171 editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
172 listenForCommonEvents(editor);
173 listenForDragAndPaste(editor, options);
175 editor.on('init', () => {
177 scrollToQueryString(editor);
178 window.editor = editor;
179 registerShortcuts(editor);
182 editor.on('PreInit', () => {
183 setupFilters(editor);
186 handleEmbedAlignmentChanges(editor);
188 // Custom handler hook
189 window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
191 // Inline code format button
192 editor.ui.registry.addButton('inlinecode', {
193 tooltip: 'Inline code',
196 editor.execCommand('mceToggleFormat', false, 'code');
203 * @param {WysiwygConfigOptions} options
205 function getContentStyle(options) {
207 html, body, html.dark-mode {
208 background: ${options.darkMode ? '#222' : '#fff'};
211 padding-left: 15px !important;
212 padding-right: 15px !important;
213 height: initial !important;
215 margin-left: auto! important;
216 margin-right: auto !important;
217 overflow-y: hidden !important;
218 }`.trim().replace('\n', '');
222 * @param {WysiwygConfigOptions} options
225 export function buildForEditor(options) {
227 window.tinymce.addI18n(options.language, options.translationMap);
230 const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
232 // Return config object
236 selector: '#html-editor',
237 cache_suffix: `?version=${version}`,
239 window.baseUrl('/dist/styles.css'),
242 skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
243 body_class: 'page-content',
244 browser_spellcheck: true,
245 relative_urls: false,
246 language: options.language,
247 directionality: options.textDirection,
248 remove_script_host: false,
249 document_base_url: window.baseUrl('/'),
250 end_container_on_empty_block: true,
251 remove_trailing_brs: false,
254 paste_data_images: false,
255 extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked|style]',
256 automatic_uploads: false,
257 custom_elements: 'doc-root,code-block',
259 '-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]',
261 '-doc-root[doc-root|#text]',
264 '+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div|hr]',
266 plugins: gatherPlugins(options),
268 toolbar: getPrimaryToolbar(options),
269 content_style: getContentStyle(options),
270 style_formats: styleFormats,
271 style_formats_merge: false,
272 media_alt_source: false,
275 table_style_by_css: true,
276 table_use_colgroups: true,
277 file_picker_types: 'file image',
279 file_picker_callback: filePickerCallback,
280 paste_preprocess(plugin, args) {
281 const {content} = args;
282 if (content.indexOf('<img src="file://') !== -1) {
286 init_instance_callback(editor) {
287 const head = editor.getDoc().querySelector('head');
288 head.innerHTML += fetchCustomHeadContent();
291 registerCustomIcons(editor);
292 registerAdditionalToolbars(editor);
293 getSetupCallback(options)(editor);
299 * @param {WysiwygConfigOptions} options
300 * @return {RawEditorOptions}
302 export function buildForInput(options) {
304 window.tinymce.addI18n(options.language, options.translationMap);
307 const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
309 // Return config object
313 target: options.containerElement,
314 cache_suffix: `?version=${version}`,
316 window.baseUrl('/dist/styles.css'),
319 skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
320 body_class: 'wysiwyg-input',
321 browser_spellcheck: true,
322 relative_urls: false,
323 language: options.language,
324 directionality: options.textDirection,
325 remove_script_host: false,
326 document_base_url: window.baseUrl('/'),
327 end_container_on_empty_block: true,
328 remove_trailing_brs: false,
331 plugins: 'link autolink lists',
333 toolbar: 'bold italic link bullist numlist',
334 content_style: getContentStyle(options),
335 file_picker_types: 'file',
336 file_picker_callback: filePickerCallback,
337 init_instance_callback(editor) {
338 const head = editor.getDoc().querySelector('head');
339 head.innerHTML += fetchCustomHeadContent();
341 editor.contentDocument.documentElement.classList.toggle('dark-mode', options.darkMode);
347 * @typedef {Object} WysiwygConfigOptions
348 * @property {Element} containerElement
349 * @property {string} language
350 * @property {boolean} darkMode
351 * @property {string} textDirection
352 * @property {string} drawioUrl
353 * @property {int} pageId
354 * @property {Object} translations
355 * @property {Object} translationMap