1 import {register as registerShortcuts} from "./shortcuts";
2 import {listen as listenForCommonEvents} from "./common-events";
3 import {scrollToQueryString, fixScrollForMobile} 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"},
19 {title: "Inline Code", inline: "code"},
21 title: "Callouts", items: [
22 {title: "Information", format: 'calloutinfo'},
23 {title: "Success", format: 'calloutsuccess'},
24 {title: "Warning", format: 'calloutwarning'},
25 {title: "Danger", format: 'calloutdanger'}
31 codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
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 buildToolbar(options) {
67 const textDirPlugins = options.textDirection === 'rtl' ? 'ltr rtl' : '';
72 'bold italic underline strikethrough superscript subscript',
73 'forecolor backcolor',
74 'alignleft aligncenter alignright alignjustify',
75 'bullist numlist outdent indent',
77 'table imagemanager-insert link hr codeeditor drawio media',
78 'removeformat code about fullscreen'
81 return toolbar.filter(row => Boolean(row)).join(' | ');
85 * @param {WysiwygConfigOptions} options
88 function gatherPlugins(options) {
105 options.textDirection === 'rtl' ? 'directionality' : '',
108 window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin(options));
109 window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
110 window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
111 window.tinymce.PluginManager.add('about', getAboutPlugin(options));
113 if (options.drawioUrl) {
114 window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
115 plugins.push('drawio');
118 return plugins.filter(plugin => Boolean(plugin)).join(' ');
122 * Load custom HTML head content from the settings into the editor.
123 * TODO: We should be able to get this from current parent page?
124 * @param {Editor} editor
126 function loadCustomHeadContent(editor) {
127 window.$http.get(window.baseUrl('/custom-head-content')).then(resp => {
128 if (!resp.data) return;
129 let head = editor.getDoc().querySelector('head');
130 head.innerHTML += resp.data;
135 * @param {WysiwygConfigOptions} options
136 * @return {function(Editor)}
138 function getSetupCallback(options) {
139 return function(editor) {
140 editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
141 listenForCommonEvents(editor);
142 registerShortcuts(editor);
143 listenForDragAndPaste(editor, options);
145 editor.on('init', () => {
147 scrollToQueryString(editor);
148 fixScrollForMobile(editor);
149 window.editor = editor;
152 function editorChange() {
153 const content = editor.getContent();
154 if (options.darkMode) {
155 editor.contentDocument.documentElement.classList.add('dark-mode');
157 window.$events.emit('editor-html-change', content);
160 // TODO - Update to standardise across both editors
161 // Use events within listenForBookStackEditorEvents instead (Different event signature)
162 window.$events.listen('editor-html-update', html => {
163 editor.setContent(html);
164 editor.selection.select(editor.getBody(), true);
165 editor.selection.collapse(false);
169 // Custom handler hook
170 window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
175 * @param {WysiwygConfigOptions} options
178 export function build(options) {
181 window.tinymce.addI18n(options.language, options.translationMap);
183 // Return config object
187 selector: '#html-editor',
189 window.baseUrl('/dist/styles.css'),
192 skin: options.darkMode ? 'oxide-dark' : 'oxide',
193 body_class: 'page-content',
194 browser_spellcheck: true,
195 relative_urls: false,
196 language: options.language,
197 directionality: options.textDirection,
198 remove_script_host: false,
199 document_base_url: window.baseUrl('/'),
200 end_container_on_empty_block: true,
203 paste_data_images: false,
204 extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
205 automatic_uploads: false,
206 valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
207 plugins: gatherPlugins(options),
208 imagetools_toolbar: 'imageoptions',
210 toolbar: buildToolbar(options),
211 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;}`,
213 style_formats_merge: false,
214 media_alt_source: false,
217 file_picker_types: 'file image',
218 file_picker_callback,
219 paste_preprocess(plugin, args) {
220 let content = args.content;
221 if (content.indexOf('<img src="file://') !== -1) {
225 init_instance_callback(editor) {
226 loadCustomHeadContent(editor);
228 setup: getSetupCallback(options),
233 * @typedef {Object} WysiwygConfigOptions
234 * @property {Element} containerElement
235 * @property {string} language
236 * @property {boolean} darkMode
237 * @property {string} textDirection
238 * @property {string} drawioUrl
239 * @property {int} pageId
240 * @property {Object} translations
241 * @property {Object} translationMap