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";
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";
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"},
22 title: "Callouts", items: [
23 {title: "Information", format: 'calloutinfo'},
24 {title: "Success", format: 'calloutsuccess'},
25 {title: "Warning", format: 'calloutwarning'},
26 {title: "Danger", format: 'calloutdanger'}
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'}}
41 function file_picker_callback(callback, value, meta) {
43 // field_name, url, type, win
44 if (meta.filetype === 'file') {
45 window.EntitySelectorPopup.show(entity => {
46 callback(entity.link, {
53 if (meta.filetype === 'image') {
55 window.ImageManager.show(function (image) {
56 callback(image.url, {alt: image.name});
63 * @param {WysiwygConfigOptions} options
66 function gatherPlugins(options) {
84 options.textDirection === 'rtl' ? 'directionality' : '',
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));
93 if (options.drawioUrl) {
94 window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
95 plugins.push('drawio');
98 return plugins.filter(plugin => Boolean(plugin)).join(' ');
102 * Fetch custom HTML head content from the parent page head into the editor.
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) {
111 return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
115 * @param {WysiwygConfigOptions} options
116 * @return {function(Editor)}
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);
125 editor.on('init', () => {
127 scrollToQueryString(editor);
128 window.editor = editor;
131 function editorChange() {
132 const content = editor.getContent();
133 if (options.darkMode) {
134 editor.contentDocument.documentElement.classList.add('dark-mode');
136 window.$events.emit('editor-html-change', content);
139 // Custom handler hook
140 window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
142 // Inline code format button
143 editor.ui.registry.addButton('inlinecode', {
144 tooltip: 'Inline code',
147 editor.execCommand('mceToggleFormat', false, 'code');
154 * @param {WysiwygConfigOptions} options
156 function getContentStyle(options) {
158 html, body, html.dark-mode {
159 background: ${options.darkMode ? '#222' : '#fff'};
162 padding-left: 15px !important;
163 padding-right: 15px !important;
164 height: initial !important;
166 margin-left: auto! important;
167 margin-right: auto !important;
168 overflow-y: hidden !important;
169 }`.trim().replace('\n', '');
173 * @param {WysiwygConfigOptions} options
176 export function build(options) {
179 window.tinymce.addI18n(options.language, options.translationMap);
181 // Return config object
185 selector: '#html-editor',
187 window.baseUrl('/dist/styles.css'),
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,
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',
206 "-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]",
208 "-doc-root[doc-root|#text]",
211 "+doc-root[code-block]"
213 plugins: gatherPlugins(options),
214 imagetools_toolbar: 'imageoptions',
216 toolbar: getPrimaryToolbar(options),
217 content_style: getContentStyle(options),
219 style_formats_merge: false,
220 media_alt_source: false,
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) {
231 init_instance_callback(editor) {
232 const head = editor.getDoc().querySelector('head');
233 head.innerHTML += fetchCustomHeadContent();
236 registerAdditionalToolbars(editor, options);
237 getSetupCallback(options)(editor);
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