]> BookStack Code Mirror - bookstack/blobdiff - resources/js/wysiwyg/config.js
Added language list favourites sorting, updated styles
[bookstack] / resources / js / wysiwyg / config.js
index 519e85a5f1ffcb93877ea378e5c20fc7c5a17cd8..66c22d98eac74991fece09f56a0619378f9a5a71 100644 (file)
@@ -1,13 +1,16 @@
 import {register as registerShortcuts} from "./shortcuts";
 import {listen as listenForCommonEvents} from "./common-events";
-import {scrollToQueryString, fixScrollForMobile} from "./scrolling";
+import {scrollToQueryString} from "./scrolling";
 import {listenForDragAndPaste} from "./drop-paste-handling";
+import {getPrimaryToolbar, registerAdditionalToolbars} from "./toolbars";
 
 import {getPlugin as getCodeeditorPlugin} from "./plugin-codeeditor";
 import {getPlugin as getDrawioPlugin} from "./plugin-drawio";
 import {getPlugin as getCustomhrPlugin} from "./plugins-customhr";
 import {getPlugin as getImagemanagerPlugin} from "./plugins-imagemanager";
 import {getPlugin as getAboutPlugin} from "./plugins-about";
+import {getPlugin as getDetailsPlugin} from "./plugins-details";
+import {getPlugin as getTasklistPlugin} from "./plugins-tasklist";
 
 const style_formats = [
     {title: "Large Header", format: "h2", preview: 'color: blue;'},
@@ -27,7 +30,6 @@ const style_formats = [
 ];
 
 const formats = {
-    codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
     alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
     aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
     alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
@@ -60,57 +62,12 @@ function file_picker_callback(callback, value, meta) {
 
 /**
  * @param {WysiwygConfigOptions} options
- * @return {{toolbar: string, groupButtons: Object<string, Object>}}
- */
-function buildToolbar(options) {
-    const textDirPlugins = options.textDirection === 'rtl' ? 'ltr rtl' : '';
-
-    const groupButtons = {
-        formatoverflow: {
-            icon: 'more-drawer',
-            tooltip: 'More',
-            items: 'strikethrough superscript subscript inlinecode removeformat'
-        },
-        listoverflow: {
-            icon: 'more-drawer',
-            tooltip: 'More',
-            items: 'outdent indent'
-        },
-        insertoverflow: {
-            icon: 'more-drawer',
-            tooltip: 'More',
-            items: 'hr codeeditor drawio media'
-        }
-    };
-
-    const toolbar = [
-        'undo redo',
-        'styleselect',
-        'bold italic underline formatoverflow',
-        'forecolor backcolor',
-        'alignleft aligncenter alignright alignjustify',
-        'bullist numlist listoverflow',
-        textDirPlugins,
-        'link table imagemanager-insert insertoverflow',
-        'code about fullscreen'
-    ];
-
-    return {
-        toolbar: toolbar.filter(row => Boolean(row)).join(' | '),
-        groupButtons,
-    };
-}
-
-/**
- * @param {WysiwygConfigOptions} options
- * @return {string}
+ * @return {string[]}
  */
 function gatherPlugins(options) {
     const plugins = [
         "image",
-        "imagetools",
         "table",
-        "paste",
         "link",
         "autolink",
         "fullscreen",
@@ -122,6 +79,8 @@ function gatherPlugins(options) {
         "media",
         "imagemanager",
         "about",
+        "details",
+        "tasklist",
         options.textDirection === 'rtl' ? 'directionality' : '',
     ];
 
@@ -129,25 +88,44 @@ function gatherPlugins(options) {
     window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
     window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
     window.tinymce.PluginManager.add('about', getAboutPlugin(options));
+    window.tinymce.PluginManager.add('details', getDetailsPlugin(options));
+    window.tinymce.PluginManager.add('tasklist', getTasklistPlugin(options));
 
     if (options.drawioUrl) {
         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
         plugins.push('drawio');
     }
 
-    return plugins.filter(plugin => Boolean(plugin)).join(' ');
+    return plugins.filter(plugin => Boolean(plugin));
+}
+
+/**
+ * Fetch custom HTML head content from the parent page head into the editor.
+ */
+function fetchCustomHeadContent() {
+    const headContentLines = document.head.innerHTML.split("\n");
+    const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
+    const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
+    if (startLineIndex === -1 || endLineIndex === -1) {
+        return ''
+    }
+    return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
 }
 
 /**
- * Load custom HTML head content from the settings into the editor.
- * TODO: We should be able to get this from current parent page?
+ * Setup a serializer filter for <br> tags to ensure they're not rendered
+ * within code blocks and that we use newlines there instead.
  * @param {Editor} editor
  */
-function loadCustomHeadContent(editor) {
-    window.$http.get(window.baseUrl('/custom-head-content')).then(resp => {
-        if (!resp.data) return;
-        let head = editor.getDoc().querySelector('head');
-        head.innerHTML += resp.data;
+function setupBrFilter(editor) {
+    editor.serializer.addNodeFilter('br', function(nodes) {
+        for (const node of nodes) {
+            if (node.parent && node.parent.name === 'code') {
+                const newline = tinymce.html.Node.create('#text');
+                newline.value = '\n';
+                node.replace(newline);
+            }
+        }
     });
 }
 
@@ -165,10 +143,13 @@ function getSetupCallback(options) {
         editor.on('init', () => {
             editorChange();
             scrollToQueryString(editor);
-            fixScrollForMobile(editor);
             window.editor = editor;
         });
 
+        editor.on('PreInit', () => {
+            setupBrFilter(editor);
+        });
+
         function editorChange() {
             const content = editor.getContent();
             if (options.darkMode) {
@@ -177,15 +158,6 @@ function getSetupCallback(options) {
             window.$events.emit('editor-html-change', content);
         }
 
-        // TODO - Update to standardise across both editors
-        // Use events within listenForBookStackEditorEvents instead (Different event signature)
-        window.$events.listen('editor-html-update', html => {
-            editor.setContent(html);
-            editor.selection.select(editor.getBody(), true);
-            editor.selection.collapse(false);
-            editorChange(html);
-        });
-
         // Custom handler hook
         window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
 
@@ -200,6 +172,25 @@ function getSetupCallback(options) {
     }
 }
 
+/**
+ * @param {WysiwygConfigOptions} options
+ */
+function getContentStyle(options) {
+    return `
+html, body, html.dark-mode {
+    background: ${options.darkMode ? '#222' : '#fff'};
+} 
+body {
+    padding-left: 15px !important;
+    padding-right: 15px !important; 
+    height: initial !important;
+    margin:0!important; 
+    margin-left: auto! important;
+    margin-right: auto !important;
+    overflow-y: hidden !important;
+}`.trim().replace('\n', '');
+}
+
 /**
  * @param {WysiwygConfigOptions} options
  * @return {Object}
@@ -209,18 +200,20 @@ export function build(options) {
     // Set language
     window.tinymce.addI18n(options.language, options.translationMap);
 
-    const {toolbar, groupButtons: toolBarGroupButtons} = buildToolbar(options);
+    // BookStack Version
+    const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
 
     // Return config object
     return {
         width: '100%',
         height: '100%',
         selector: '#html-editor',
+        cache_suffix: '?version=' + version,
         content_css: [
             window.baseUrl('/dist/styles.css'),
         ],
         branding: false,
-        skin: options.darkMode ? 'oxide-dark' : 'oxide',
+        skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
         body_class: 'page-content',
         browser_spellcheck: true,
         relative_urls: false,
@@ -229,37 +222,46 @@ export function build(options) {
         remove_script_host: false,
         document_base_url: window.baseUrl('/'),
         end_container_on_empty_block: true,
+        remove_trailing_brs: false,
         statusbar: false,
         menubar: false,
         paste_data_images: false,
-        extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
+        extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked]',
         automatic_uploads: false,
-        valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
+        custom_elements: 'doc-root,code-block',
+        valid_children: [
+            "-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]",
+            "+div[pre|img]",
+            "-doc-root[doc-root|#text]",
+            "-li[details]",
+            "+code-block[pre]",
+            "+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div]"
+        ].join(','),
         plugins: gatherPlugins(options),
-        imagetools_toolbar: 'imageoptions',
         contextmenu: false,
-        toolbar: toolbar,
-        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;}`,
+        toolbar: getPrimaryToolbar(options),
+        content_style: getContentStyle(options),
         style_formats,
         style_formats_merge: false,
         media_alt_source: false,
         media_poster: false,
         formats,
+        table_style_by_css: true,
+        table_use_colgroups: true,
         file_picker_types: 'file image',
         file_picker_callback,
         paste_preprocess(plugin, args) {
-            let content = args.content;
+            const content = args.content;
             if (content.indexOf('<img src="file://') !== -1) {
                 args.content = '';
             }
         },
         init_instance_callback(editor) {
-            loadCustomHeadContent(editor);
+            const head = editor.getDoc().querySelector('head');
+            head.innerHTML += fetchCustomHeadContent();
         },
         setup(editor) {
-            for (const [key, config] of Object.entries(toolBarGroupButtons)) {
-                editor.ui.registry.addGroupToolbarButton(key, config);
-            }
+            registerAdditionalToolbars(editor, options);
             getSetupCallback(options)(editor);
         },
     };