]> BookStack Code Mirror - bookstack/blobdiff - resources/assets/js/components/wysiwyg-editor.js
Fixed issue causing text overlap in sort select box
[bookstack] / resources / assets / js / components / wysiwyg-editor.js
index 69286abffff6227051ebcf722c20fa6d8a367082..c03c0d2aa4e8bba75b8206e7060d50bb4dc26f06 100644 (file)
@@ -1,25 +1,36 @@
-const Code = require('../services/code');
-const DrawIO = require('../services/drawio');
+import Code from "../services/code";
+import DrawIO from "../services/drawio";
 
 /**
  * Handle pasting images from clipboard.
  * @param {ClipboardEvent} event
+ * @param {WysiwygEditor} wysiwygComponent
  * @param editor
  */
-function editorPaste(event, editor) {
-    if (!event.clipboardData || !event.clipboardData.items) return;
-    let items = event.clipboardData.items;
+function editorPaste(event, editor, wysiwygComponent) {
+    const clipboardItems = event.clipboardData.items;
+    if (!event.clipboardData || !clipboardItems) return;
 
-    for (let i = 0; i < items.length; i++) {
-        if (items[i].type.indexOf("image") === -1) continue;
-        event.preventDefault();
+    // Don't handle if clipboard includes text content
+    for (let clipboardItem of clipboardItems) {
+        if (clipboardItem.type.includes('text/')) {
+            return;
+        }
+    }
+
+    for (let clipboardItem of clipboardItems) {
+        if (!clipboardItem.type.includes("image")) {
+            continue;
+        }
+
+        const id = "image-" + Math.random().toString(16).slice(2);
+        const loadingImage = window.baseUrl('/loading.gif');
+        const file = clipboardItem.getAsFile();
 
-        let id = "image-" + Math.random().toString(16).slice(2);
-        let loadingImage = window.baseUrl('/loading.gif');
-        let file = items[i].getAsFile();
         setTimeout(() => {
             editor.insertContent(`<p><img src="${loadingImage}" id="${id}"></p>`);
-            uploadImageFile(file).then(resp => {
+
+            uploadImageFile(file, wysiwygComponent).then(resp => {
                 editor.dom.setAttrib(id, 'src', resp.thumbs.display);
             }).catch(err => {
                 editor.dom.remove(id);
@@ -33,9 +44,12 @@ function editorPaste(event, editor) {
 /**
  * Upload an image file to the server
  * @param {File} file
+ * @param {WysiwygEditor} wysiwygComponent
  */
-function uploadImageFile(file) {
-    if (file === null || file.type.indexOf('image') !== 0) return Promise.reject(`Not an image file`);
+async function uploadImageFile(file, wysiwygComponent) {
+    if (file === null || file.type.indexOf('image') !== 0) {
+        throw new Error(`Not an image file`);
+    }
 
     let ext = 'png';
     if (file.name) {
@@ -43,11 +57,13 @@ function uploadImageFile(file) {
         if (fileNameMatches.length > 1) ext = fileNameMatches[1];
     }
 
-    let remoteFilename = "image-" + Date.now() + "." + ext;
-    let formData = new FormData();
+    const remoteFilename = "image-" + Date.now() + "." + ext;
+    const formData = new FormData();
     formData.append('file', file, remoteFilename);
+    formData.append('uploaded_to', wysiwygComponent.pageId);
 
-    return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (resp.data));
+    const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);
+    return resp.data;
 }
 
 function registerEditorShortcuts(editor) {
@@ -152,23 +168,24 @@ function codePlugin() {
         });
     }
 
-    function codeMirrorContainerToPre($codeMirrorContainer) {
-        let textArea = $codeMirrorContainer[0].querySelector('textarea');
-        let code = textArea.textContent;
-        let lang = $codeMirrorContainer[0].getAttribute('data-lang');
-
-        $codeMirrorContainer.removeAttr('contentEditable');
-        let $pre = $('<pre></pre>');
-        $pre.append($('<code></code>').each((index, elem) => {
-            // Needs to be textContent since innerText produces BR:s
-            elem.textContent = code;
-        }).attr('class', `language-${lang}`));
-        $codeMirrorContainer.replaceWith($pre);
+    function codeMirrorContainerToPre(codeMirrorContainer) {
+        const textArea = codeMirrorContainer.querySelector('textarea');
+        const code = textArea.textContent;
+        const lang = codeMirrorContainer.getAttribute('data-lang');
+
+        codeMirrorContainer.removeAttribute('contentEditable');
+        const pre = document.createElement('pre');
+        const codeElem = document.createElement('code');
+        codeElem.classList.add(`language-${lang}`);
+        codeElem.textContent = code;
+        pre.appendChild(codeElem);
+
+        codeMirrorContainer.parentElement.replaceChild(pre, codeMirrorContainer);
     }
 
     window.tinymce.PluginManager.add('codeeditor', function(editor, url) {
 
-        let $ = editor.$;
+        const $ = editor.$;
 
         editor.addButton('codeeditor', {
             text: 'Code block',
@@ -182,10 +199,8 @@ function codePlugin() {
 
         // Convert
         editor.on('PreProcess', function (e) {
-            $('div.CodeMirrorContainer', e.node).
-            each((index, elem) => {
-                let $elem = $(elem);
-                codeMirrorContainerToPre($elem);
+            $('div.CodeMirrorContainer', e.node).each((index, elem) => {
+                codeMirrorContainerToPre(elem);
             });
         });
 
@@ -201,10 +216,10 @@ function codePlugin() {
             $('.CodeMirrorContainer').filter((index ,elem) => {
                 return typeof elem.querySelector('.CodeMirror').CodeMirror === 'undefined';
             }).each((index, elem) => {
-                codeMirrorContainerToPre($(elem));
+                codeMirrorContainerToPre(elem);
             });
 
-            let codeSamples = $('body > pre').filter((index, elem) => {
+            const codeSamples = $('body > pre').filter((index, elem) => {
                 return elem.contentEditable !== "false";
             });
 
@@ -250,39 +265,38 @@ function drawIoPlugin() {
         DrawIO.show(drawingInit, updateContent);
     }
 
-    function updateContent(pngData) {
-        let id = "image-" + Math.random().toString(16).slice(2);
-        let loadingImage = window.baseUrl('/loading.gif');
-        let data = {
-            image: pngData,
-            uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
-        };
+    async function updateContent(pngData) {
+        const id = "image-" + Math.random().toString(16).slice(2);
+        const loadingImage = window.baseUrl('/loading.gif');
+        const pageId = Number(document.getElementById('page-editor').getAttribute('page-id'));
 
         // Handle updating an existing image
         if (currentNode) {
             DrawIO.close();
             let imgElem = currentNode.querySelector('img');
-            window.$http.post(window.baseUrl(`/images/drawing/upload`), data).then(resp => {
-                pageEditor.dom.setAttrib(imgElem, 'src', resp.data.url);
-                pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', resp.data.id);
-            }).catch(err => {
+            try {
+                const img = await DrawIO.upload(pngData, pageId);
+                pageEditor.dom.setAttrib(imgElem, 'src', img.url);
+                pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
+            } catch (err) {
                 window.$events.emit('error', trans('errors.image_upload_error'));
                 console.log(err);
-            });
+            }
             return;
         }
 
-        setTimeout(() => {
+        setTimeout(async () => {
             pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
             DrawIO.close();
-            window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => {
-                pageEditor.dom.setAttrib(id, 'src', resp.data.url);
-                pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id);
-            }).catch(err => {
+            try {
+                const img = await DrawIO.upload(pngData, pageId);
+                pageEditor.dom.setAttrib(id, 'src', img.url);
+                pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
+            } catch (err) {
                 pageEditor.dom.remove(id);
                 window.$events.emit('error', trans('errors.image_upload_error'));
                 console.log(err);
-            });
+            }
         }, 5);
     }
 
@@ -293,9 +307,7 @@ function drawIoPlugin() {
         }
 
         let drawingId = currentNode.getAttribute('drawio-diagram');
-        return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {
-            return `data:image/png;base64,${resp.data.content}`;
-        });
+        return DrawIO.load(drawingId);
     }
 
     window.tinymce.PluginManager.add('drawio', function(editor, url) {
@@ -328,7 +340,7 @@ function drawIoPlugin() {
         });
 
         editor.on('SetContent', function () {
-            let drawings = editor.$('body > div[drawio-diagram]');
+            const drawings = editor.$('body > div[drawio-diagram]');
             if (!drawings.length) return;
 
             editor.undoManager.transact(function () {
@@ -366,11 +378,36 @@ function customHrPlugin() {
 }
 
 
+function listenForBookStackEditorEvents(editor) {
+
+    // Replace editor content
+    window.$events.listen('editor::replace', ({html}) => {
+        editor.setContent(html);
+    });
+
+    // Append editor content
+    window.$events.listen('editor::append', ({html}) => {
+        const content = editor.getContent() + html;
+        editor.setContent(content);
+    });
+
+    // Prepend editor content
+    window.$events.listen('editor::prepend', ({html}) => {
+        const content = html + editor.getContent();
+        editor.setContent(content);
+    });
+
+}
+
 class WysiwygEditor {
 
     constructor(elem) {
         this.elem = elem;
 
+        const pageEditor = document.getElementById('page-editor');
+        this.pageId = pageEditor.getAttribute('page-id');
+        this.textDirection = pageEditor.getAttribute('text-direction');
+
         this.plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor media";
         this.loadPlugins();
 
@@ -385,9 +422,20 @@ class WysiwygEditor {
             drawIoPlugin();
             this.plugins += ' drawio';
         }
+        if (this.textDirection === 'rtl') {
+            this.plugins += ' directionality'
+        }
+    }
+
+    getToolBar() {
+        const textDirPlugins = this.textDirection === 'rtl' ? 'ltr rtl' : '';
+        return `undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr drawio media | removeformat code ${textDirPlugins} fullscreen`
     }
 
     getTinyMceConfig() {
+
+        const context = this;
+
         return {
             selector: '#html-editor',
             content_css: [
@@ -397,8 +445,10 @@ class WysiwygEditor {
             body_class: 'page-content',
             browser_spellcheck: true,
             relative_urls: false,
+            directionality : this.textDirection,
             remove_script_host: false,
             document_base_url: window.baseUrl('/'),
+            end_container_on_empty_block: true,
             statusbar: false,
             menubar: false,
             paste_data_images: false,
@@ -407,8 +457,8 @@ class WysiwygEditor {
             valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
             plugins: this.plugins,
             imagetools_toolbar: 'imageoptions',
-            toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr drawio media | removeformat code fullscreen",
-            content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
+            toolbar: this.getToolBar(),
+            content_style: "html, body {background: #FFF;} body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
             style_formats: [
                 {title: "Header Large", format: "h2"},
                 {title: "Header Medium", format: "h3"},
@@ -442,9 +492,10 @@ class WysiwygEditor {
 
                 if (type === 'file') {
                     window.EntitySelectorPopup.show(function(entity) {
-                        let originalField = win.document.getElementById(field_name);
+                        const originalField = win.document.getElementById(field_name);
                         originalField.value = entity.link;
-                        $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
+                        const mceForm = originalField.closest('.mce-form');
+                        mceForm.querySelectorAll('input')[2].value = entity.name;
                     });
                 }
 
@@ -483,22 +534,35 @@ class WysiwygEditor {
             },
             setup: function (editor) {
 
-                editor.on('init ExecCommand change input NodeChange ObjectResized', editorChange);
+                editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
+
+                editor.on('init', () => {
+                    editorChange();
+                    // Scroll to the content if needed.
+                    const queryParams = (new URL(window.location)).searchParams;
+                    const scrollId = queryParams.get('content-id');
+                    if (scrollId) {
+                        scrollToText(scrollId);
+                    }
+
+                    // Override for touch events to allow scroll on mobile
+                    const container = editor.getContainer();
+                    const toolbarButtons = container.querySelectorAll('.mce-btn');
+                    for (let button of toolbarButtons) {
+                        button.addEventListener('touchstart', event => {
+                            event.stopPropagation();
+                        });
+                    }
+                    window.editor = editor;
+                });
 
                 function editorChange() {
                     let content = editor.getContent();
                     window.$events.emit('editor-html-change', content);
                 }
 
-                window.$events.listen('editor-html-update', html => {
-                    editor.setContent(html);
-                    editor.selection.select(editor.getBody(), true);
-                    editor.selection.collapse(false);
-                    editorChange(html);
-                });
-
-                window.$events.listen('editor-scroll-to-text', textId => {
-                    const element = editor.dom.get(textId)
+                function scrollToText(scrollId) {
+                    const element = editor.dom.get(encodeURIComponent(scrollId).replace(/!/g, '%21'));
                     if (!element) {
                         return;
                     }
@@ -508,6 +572,17 @@ class WysiwygEditor {
                     editor.selection.select(element, true);
                     editor.selection.collapse(false);
                     editor.focus();
+                }
+
+                listenForBookStackEditorEvents(editor);
+
+                // 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);
                 });
 
                 registerEditorShortcuts(editor);
@@ -533,6 +608,18 @@ class WysiwygEditor {
                     let dom = editor.dom,
                         rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
 
+                    // Template insertion
+                    const templateId = event.dataTransfer.getData('bookstack/template');
+                    if (templateId) {
+                        event.preventDefault();
+                        window.$http.get(`/templates/${templateId}`).then(resp => {
+                            editor.selection.setRng(rng);
+                            editor.undoManager.transact(function () {
+                                editor.execCommand('mceInsertContent', false, resp.data.html);
+                            });
+                        });
+                    }
+
                     // Don't allow anything to be dropped in a captioned image.
                     if (dom.getParent(rng.startContainer, '.mceTemp')) {
                         event.preventDefault();
@@ -565,11 +652,12 @@ class WysiwygEditor {
                 });
 
                 // Paste image-uploads
-                editor.on('paste', event => editorPaste(event, editor));
+                editor.on('paste', event => editorPaste(event, editor, context));
+
             }
         };
     }
 
 }
 
-module.exports = WysiwygEditor;
+export default WysiwygEditor;