]> BookStack Code Mirror - bookstack/blobdiff - resources/js/wysiwyg/config.js
Comments: Added HTML filter on load, tinymce elem filtering
[bookstack] / resources / js / wysiwyg / config.js
index 52c52592cb720fab6a235a5d04ea4902cbf89515..fa2df9c11721277bab2ed7770d1d6a0787b3fada 100644 (file)
@@ -1,45 +1,51 @@
-import {register as registerShortcuts} from "./shortcuts";
-import {listen as listenForCommonEvents} from "./common-events";
-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;'},
-    {title: "Medium Header", format: "h3"},
-    {title: "Small Header", format: "h4"},
-    {title: "Tiny Header", format: "h5"},
-    {title: "Paragraph", format: "p", exact: true, classes: ''},
-    {title: "Blockquote", format: "blockquote"},
+import {register as registerShortcuts} from './shortcuts';
+import {listen as listenForCommonEvents} from './common-events';
+import {scrollToQueryString} from './scrolling';
+import {listenForDragAndPaste} from './drop-paste-handling';
+import {getPrimaryToolbar, registerAdditionalToolbars} from './toolbars';
+import {registerCustomIcons} from './icons';
+import {setupFilters} from './filters';
+
+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';
+import {handleEmbedAlignmentChanges} from './fixes';
+
+const styleFormats = [
+    {title: 'Large Header', format: 'h2', preview: 'color: blue;'},
+    {title: 'Medium Header', format: 'h3'},
+    {title: 'Small Header', format: 'h4'},
+    {title: 'Tiny Header', format: 'h5'},
     {
-        title: "Callouts", items: [
-            {title: "Information", format: 'calloutinfo'},
-            {title: "Success", format: 'calloutsuccess'},
-            {title: "Warning", format: 'calloutwarning'},
-            {title: "Danger", format: 'calloutdanger'}
-        ]
+        title: 'Paragraph', format: 'p', exact: true, classes: '',
+    },
+    {title: 'Blockquote', format: 'blockquote'},
+    {
+        title: 'Callouts',
+        items: [
+            {title: 'Information', format: 'calloutinfo'},
+            {title: 'Success', format: 'calloutsuccess'},
+            {title: 'Warning', format: 'calloutwarning'},
+            {title: 'Danger', format: 'calloutdanger'},
+        ],
     },
 ];
 
 const formats = {
-    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'},
+    alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-left'},
+    aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-center'},
+    alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-right'},
     calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
     calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
     calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
-    calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
+    calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}},
 };
 
-const color_map = [
+const colorMap = [
     '#BFEDD2', '',
     '#FBEEB8', '',
     '#F8CAC6', '',
@@ -65,28 +71,36 @@ const color_map = [
     '#34495E', '',
 
     '#000000', '',
-    '#ffffff', ''
+    '#ffffff', '',
 ];
 
-function file_picker_callback(callback, value, meta) {
-
+function filePickerCallback(callback, value, meta) {
     // field_name, url, type, win
     if (meta.filetype === 'file') {
-        window.EntitySelectorPopup.show(entity => {
+        /** @type {EntitySelectorPopup} * */
+        const selector = window.$components.first('entity-selector-popup');
+        const selectionText = this.selection.getContent({format: 'text'}).trim();
+        selector.show(entity => {
             callback(entity.link, {
                 text: entity.name,
                 title: entity.name,
             });
+        }, {
+            initialValue: selectionText,
+            searchEndpoint: '/search/entity-selector',
+            entityTypes: 'page,book,chapter,bookshelf',
+            entityPermission: 'view',
         });
     }
 
     if (meta.filetype === 'image') {
         // Show image manager
-        window.ImageManager.show(function (image) {
+        /** @type {ImageManager} * */
+        const imageManager = window.$components.first('image-manager');
+        imageManager.show(image => {
             callback(image.url, {alt: image.name});
         }, 'gallery');
     }
-
 }
 
 /**
@@ -95,30 +109,30 @@ function file_picker_callback(callback, value, meta) {
  */
 function gatherPlugins(options) {
     const plugins = [
-        "image",
-        "table",
-        "link",
-        "autolink",
-        "fullscreen",
-        "code",
-        "customhr",
-        "autosave",
-        "lists",
-        "codeeditor",
-        "media",
-        "imagemanager",
-        "about",
-        "details",
-        "tasklist",
+        'image',
+        'table',
+        'link',
+        'autolink',
+        'fullscreen',
+        'code',
+        'customhr',
+        'autosave',
+        'lists',
+        'codeeditor',
+        'media',
+        'imagemanager',
+        'about',
+        'details',
+        'tasklist',
         options.textDirection === 'rtl' ? 'directionality' : '',
     ];
 
-    window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin(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));
+    window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin());
+    window.tinymce.PluginManager.add('customhr', getCustomhrPlugin());
+    window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin());
+    window.tinymce.PluginManager.add('about', getAboutPlugin());
+    window.tinymce.PluginManager.add('details', getDetailsPlugin());
+    window.tinymce.PluginManager.add('tasklist', getTasklistPlugin());
 
     if (options.drawioUrl) {
         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
@@ -129,33 +143,23 @@ function gatherPlugins(options) {
 }
 
 /**
- * Fetch custom HTML head content from the parent page head into the editor.
+ * Fetch custom HTML head content nodes from the outer page head
+ * and add them to the given editor document.
+ * @param {Document} editorDoc
  */
-function fetchCustomHeadContent() {
-    const headContentLines = document.head.innerHTML.split("\n");
+function addCustomHeadContent(editorDoc) {
+    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;
     }
-    return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
-}
 
-/**
- * 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 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);
-            }
-        }
-    });
+    const customHeadHtml = headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
+    const el = editorDoc.createElement('div');
+    el.innerHTML = customHeadHtml;
+
+    editorDoc.head.append(...el.children);
 }
 
 /**
@@ -163,7 +167,14 @@ function setupBrFilter(editor) {
  * @return {function(Editor)}
  */
 function getSetupCallback(options) {
-    return function(editor) {
+    return function setupCallback(editor) {
+        function editorChange() {
+            if (options.darkMode) {
+                editor.contentDocument.documentElement.classList.add('dark-mode');
+            }
+            window.$events.emit('editor-html-change', '');
+        }
+
         editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
         listenForCommonEvents(editor);
         listenForDragAndPaste(editor, options);
@@ -176,16 +187,10 @@ function getSetupCallback(options) {
         });
 
         editor.on('PreInit', () => {
-            setupBrFilter(editor);
+            setupFilters(editor);
         });
 
-        function editorChange() {
-            const content = editor.getContent();
-            if (options.darkMode) {
-                editor.contentDocument.documentElement.classList.add('dark-mode');
-            }
-            window.$events.emit('editor-html-change', content);
-        }
+        handleEmbedAlignmentChanges(editor);
 
         // Custom handler hook
         window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
@@ -196,9 +201,9 @@ function getSetupCallback(options) {
             icon: 'sourcecode',
             onAction() {
                 editor.execCommand('mceToggleFormat', false, 'code');
-            }
-        })
-    }
+            },
+        });
+    };
 }
 
 /**
@@ -224,8 +229,7 @@ body {
  * @param {WysiwygConfigOptions} options
  * @return {Object}
  */
-export function build(options) {
-
+export function buildForEditor(options) {
     // Set language
     window.tinymce.addI18n(options.language, options.translationMap);
 
@@ -237,7 +241,7 @@ export function build(options) {
         width: '100%',
         height: '100%',
         selector: '#html-editor',
-        cache_suffix: '?version=' + version,
+        cache_suffix: `?version=${version}`,
         content_css: [
             window.baseUrl('/dist/styles.css'),
         ],
@@ -255,22 +259,22 @@ export function build(options) {
         statusbar: false,
         menubar: false,
         paste_data_images: false,
-        extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked]',
+        extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked|style]',
         automatic_uploads: false,
         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]"
+            '-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|hr]',
         ].join(','),
         plugins: gatherPlugins(options),
         contextmenu: false,
         toolbar: getPrimaryToolbar(options),
         content_style: getContentStyle(options),
-        style_formats,
+        style_formats: styleFormats,
         style_formats_merge: false,
         media_alt_source: false,
         media_poster: false,
@@ -278,25 +282,73 @@ export function build(options) {
         table_style_by_css: true,
         table_use_colgroups: true,
         file_picker_types: 'file image',
-        color_map,
-        file_picker_callback,
+        color_map: colorMap,
+        file_picker_callback: filePickerCallback,
         paste_preprocess(plugin, args) {
-            const content = args.content;
+            const {content} = args;
             if (content.indexOf('<img src="file://') !== -1) {
                 args.content = '';
             }
         },
         init_instance_callback(editor) {
-            const head = editor.getDoc().querySelector('head');
-            head.innerHTML += fetchCustomHeadContent();
+            addCustomHeadContent(editor.getDoc());
         },
         setup(editor) {
-            registerAdditionalToolbars(editor, options);
+            registerCustomIcons(editor);
+            registerAdditionalToolbars(editor);
             getSetupCallback(options)(editor);
         },
     };
 }
 
+/**
+ * @param {WysiwygConfigOptions} options
+ * @return {RawEditorOptions}
+ */
+export function buildForInput(options) {
+    // Set language
+    window.tinymce.addI18n(options.language, options.translationMap);
+
+    // BookStack Version
+    const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
+
+    // Return config object
+    return {
+        width: '100%',
+        height: '185px',
+        target: options.containerElement,
+        cache_suffix: `?version=${version}`,
+        content_css: [
+            window.baseUrl('/dist/styles.css'),
+        ],
+        branding: false,
+        skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
+        body_class: 'wysiwyg-input',
+        browser_spellcheck: true,
+        relative_urls: false,
+        language: options.language,
+        directionality: options.textDirection,
+        remove_script_host: false,
+        document_base_url: window.baseUrl('/'),
+        end_container_on_empty_block: true,
+        remove_trailing_brs: false,
+        statusbar: false,
+        menubar: false,
+        plugins: 'link autolink lists',
+        contextmenu: false,
+        toolbar: 'bold italic link bullist numlist',
+        content_style: getContentStyle(options),
+        file_picker_types: 'file',
+        valid_elements: 'p,a[href|title],ol,ul,li,strong,em,br',
+        file_picker_callback: filePickerCallback,
+        init_instance_callback(editor) {
+            addCustomHeadContent(editor.getDoc());
+
+            editor.contentDocument.documentElement.classList.toggle('dark-mode', options.darkMode);
+        },
+    };
+}
+
 /**
  * @typedef {Object} WysiwygConfigOptions
  * @property {Element} containerElement
@@ -307,4 +359,4 @@ export function build(options) {
  * @property {int} pageId
  * @property {Object} translations
  * @property {Object} translationMap
- */
\ No newline at end of file
+ */