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";
6 import {getPlugin as getCodeeditorPlugin} from "./plugin-codeeditor";
7 import {getPlugin as getDrawioPlugin} from "./plugin-drawio";
8 import {getPlugin as getCustomhrPlugin} from "./plugins-customhr";
9 import {getPlugin as getImagemanagerPlugin} from "./plugins-imagemanager";
10 import {getPlugin as getAboutPlugin} from "./plugins-about";
12 const style_formats = [
13 {title: "Large Header", format: "h2", preview: 'color: blue;'},
14 {title: "Medium Header", format: "h3"},
15 {title: "Small Header", format: "h4"},
16 {title: "Tiny Header", format: "h5"},
17 {title: "Paragraph", format: "p", exact: true, classes: ''},
18 {title: "Blockquote", format: "blockquote"},
20 title: "Callouts", items: [
21 {title: "Information", format: 'calloutinfo'},
22 {title: "Success", format: 'calloutsuccess'},
23 {title: "Warning", format: 'calloutwarning'},
24 {title: "Danger", format: 'calloutdanger'}
30 codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
31 alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
32 aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
33 alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
34 calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
35 calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
36 calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
37 calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
40 function file_picker_callback(callback, value, meta) {
42 // field_name, url, type, win
43 if (meta.filetype === 'file') {
44 window.EntitySelectorPopup.show(entity => {
45 callback(entity.link, {
52 if (meta.filetype === 'image') {
54 window.ImageManager.show(function (image) {
55 callback(image.url, {alt: image.name});
62 * @param {WysiwygConfigOptions} options
63 * @return {{toolbar: string, groupButtons: Object<string, Object>}}
65 function buildToolbar(options) {
66 const textDirPlugins = options.textDirection === 'rtl' ? 'ltr rtl' : '';
68 const groupButtons = {
72 items: 'strikethrough superscript subscript inlinecode removeformat'
77 items: 'outdent indent'
82 items: 'hr codeeditor drawio media'
89 'bold italic underline forecolor backcolor formatoverflow',
90 'alignleft aligncenter alignright alignjustify',
91 'bullist numlist listoverflow',
93 'link table imagemanager-insert insertoverflow',
94 'code about fullscreen'
98 toolbar: toolbar.filter(row => Boolean(row)).join(' | '),
104 * @param {WysiwygConfigOptions} options
107 function gatherPlugins(options) {
124 options.textDirection === 'rtl' ? 'directionality' : '',
127 window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin(options));
128 window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
129 window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
130 window.tinymce.PluginManager.add('about', getAboutPlugin(options));
132 if (options.drawioUrl) {
133 window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
134 plugins.push('drawio');
137 return plugins.filter(plugin => Boolean(plugin)).join(' ');
141 * Fetch custom HTML head content from the parent page head into the editor.
143 function fetchCustomHeadContent() {
144 const headContentLines = document.head.innerHTML.split("\n");
145 const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
146 const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
147 if (startLineIndex === -1 || endLineIndex === -1) {
150 return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
154 * @param {WysiwygConfigOptions} options
155 * @return {function(Editor)}
157 function getSetupCallback(options) {
158 return function(editor) {
159 editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
160 listenForCommonEvents(editor);
161 registerShortcuts(editor);
162 listenForDragAndPaste(editor, options);
164 editor.on('init', () => {
166 scrollToQueryString(editor);
167 window.editor = editor;
170 function editorChange() {
171 const content = editor.getContent();
172 if (options.darkMode) {
173 editor.contentDocument.documentElement.classList.add('dark-mode');
175 window.$events.emit('editor-html-change', content);
178 // Custom handler hook
179 window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
181 // Inline code format button
182 editor.ui.registry.addButton('inlinecode', {
183 tooltip: 'Inline code',
186 editor.execCommand('mceToggleFormat', false, 'code');
193 * @param {WysiwygConfigOptions} options
196 export function build(options) {
199 window.tinymce.addI18n(options.language, options.translationMap);
201 const {toolbar, groupButtons: toolBarGroupButtons} = buildToolbar(options);
203 // Return config object
207 selector: '#html-editor',
209 window.baseUrl('/dist/styles.css'),
212 skin: options.darkMode ? 'oxide-dark' : 'oxide',
213 body_class: 'page-content',
214 browser_spellcheck: true,
215 relative_urls: false,
216 language: options.language,
217 directionality: options.textDirection,
218 remove_script_host: false,
219 document_base_url: window.baseUrl('/'),
220 end_container_on_empty_block: true,
223 paste_data_images: false,
224 extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
225 automatic_uploads: false,
226 valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
227 plugins: gatherPlugins(options),
228 imagetools_toolbar: 'imageoptions',
231 content_style: `html, body, html.dark-mode {background: ${options.darkMode ? '#222' : '#fff'};} body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}`,
233 style_formats_merge: false,
234 media_alt_source: false,
237 file_picker_types: 'file image',
238 file_picker_callback,
239 paste_preprocess(plugin, args) {
240 const content = args.content;
241 if (content.indexOf('<img src="file://') !== -1) {
245 init_instance_callback(editor) {
246 const head = editor.getDoc().querySelector('head');
247 head.innerHTML += fetchCustomHeadContent();
250 for (const [key, config] of Object.entries(toolBarGroupButtons)) {
251 editor.ui.registry.addGroupToolbarButton(key, config);
253 getSetupCallback(options)(editor);
259 * @typedef {Object} WysiwygConfigOptions
260 * @property {Element} containerElement
261 * @property {string} language
262 * @property {boolean} darkMode
263 * @property {string} textDirection
264 * @property {string} drawioUrl
265 * @property {int} pageId
266 * @property {Object} translations
267 * @property {Object} translationMap