]> BookStack Code Mirror - bookstack/blobdiff - resources/js/components/markdown-editor.js
Replaced markdown preview display iframe with div
[bookstack] / resources / js / components / markdown-editor.js
index c89a7ad8be87c41673c5e998bdef200f8bbe8f19..3f9dbeb6979dca6209c6f7450ec9c12627b90fc5 100644 (file)
@@ -1,53 +1,60 @@
 import MarkdownIt from "markdown-it";
 import mdTasksLists from 'markdown-it-task-lists';
-import code from '../services/code';
+import Clipboard from "../services/clipboard";
 import {debounce} from "../services/util";
 
 import DrawIO from "../services/drawio";
 
 class MarkdownEditor {
 
-    constructor(elem) {
-        this.elem = elem;
+    setup() {
+        this.elem = this.$el;
 
-        const pageEditor = document.getElementById('page-editor');
-        this.pageId = pageEditor.getAttribute('page-id');
-        this.textDirection = pageEditor.getAttribute('text-direction');
+        this.pageId = this.$opts.pageId;
+        this.textDirection = this.$opts.textDirection;
+        this.imageUploadErrorText = this.$opts.imageUploadErrorText;
+        this.serverUploadLimitText = this.$opts.serverUploadLimitText;
 
         this.markdown = new MarkdownIt({html: true});
         this.markdown.use(mdTasksLists, {label: true});
 
-        this.display = this.elem.querySelector('.markdown-display');
+        this.display = this.$refs.display;
+        this.input = this.$refs.input;
 
-        this.displayStylesLoaded = false;
-        this.input = this.elem.querySelector('textarea');
-        this.htmlInput = this.elem.querySelector('input[name=html]');
-        this.cm = code.markdownEditor(this.input);
+        this.cm = null;
+        this.Code = null;
+        const cmLoadPromise = window.importVersioned('code').then(Code => {
+            this.cm = Code.markdownEditor(this.input);
+            this.Code = Code;
+            return this.cm;
+        });
 
         this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
-
-        this.display.addEventListener('load', () => {
-            this.displayDoc = this.display.contentDocument;
-            this.init();
+        window.$events.emitPublic(this.elem, 'editor-markdown::setup', {
+            markdownIt: this.markdown,
+            displayEl: this.display,
+            codeMirrorInstance: this.cm,
         });
+
+        this.init(cmLoadPromise);
     }
 
-    init() {
+    init(cmLoadPromise) {
 
         let lastClick = 0;
 
         // Prevent markdown display link click redirect
-        this.displayDoc.addEventListener('click', event => {
-            let isDblClick = Date.now() - lastClick < 300;
+        this.display.addEventListener('click', event => {
+            const isDblClick = Date.now() - lastClick < 300;
 
-            let link = event.target.closest('a');
+            const link = event.target.closest('a');
             if (link !== null) {
                 event.preventDefault();
                 window.open(link.getAttribute('href'));
                 return;
             }
 
-            let drawing = event.target.closest('[drawio-diagram]');
+            const drawing = event.target.closest('[drawio-diagram]');
             if (drawing !== null && isDblClick) {
                 this.actionEditDrawing(drawing);
                 return;
@@ -58,10 +65,10 @@ class MarkdownEditor {
 
         // Button actions
         this.elem.addEventListener('click', event => {
-            let button = event.target.closest('button[data-action]');
+            const button = event.target.closest('button[data-action]');
             if (button === null) return;
 
-            let action = button.getAttribute('data-action');
+            const action = button.getAttribute('data-action');
             if (action === 'insertImage') this.actionInsertImage();
             if (action === 'insertLink') this.actionShowLinkSelector();
             if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
@@ -69,6 +76,7 @@ class MarkdownEditor {
                 return;
             }
             if (action === 'insertDrawing') this.actionStartDrawing();
+            if (action === 'fullscreen') this.actionFullScreen();
         });
 
         // Mobile section toggling
@@ -84,12 +92,15 @@ class MarkdownEditor {
             toolbarLabel.closest('.markdown-editor-wrap').classList.add('active');
         });
 
-        window.$events.listen('editor-markdown-update', value => {
-            this.cm.setValue(value);
-            this.updateAndRender();
+        cmLoadPromise.then(cm => {
+            this.codeMirrorSetup(cm);
+
+            // Refresh CodeMirror on container resize
+            const resizeDebounced = debounce(() => this.Code.updateLayout(cm), 100, false);
+            const observer = new ResizeObserver(resizeDebounced);
+            observer.observe(this.elem);
         });
 
-        this.codeMirrorSetup();
         this.listenForBookStackEditorEvents();
 
         // Scroll to text if needed.
@@ -109,45 +120,25 @@ class MarkdownEditor {
         window.$events.emit('editor-markdown-change', content);
 
         // Set body content
-        this.displayDoc.body.className = 'page-content';
-        this.displayDoc.body.innerHTML = html;
-        this.htmlInput.value = html;
-
-        // Copy styles from page head and set custom styles for editor
-        this.loadStylesIntoDisplay();
-    }
-
-    loadStylesIntoDisplay() {
-        if (this.displayStylesLoaded) return;
-        this.displayDoc.documentElement.className = 'markdown-editor-display';
-
-        this.displayDoc.head.innerHTML = '';
-        const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
-        for (let style of styles) {
-            const copy = style.cloneNode(true);
-            this.displayDoc.head.appendChild(copy);
-        }
-
-        this.displayStylesLoaded = true;
+        this.display.innerHTML = html;
     }
 
     onMarkdownScroll(lineCount) {
-        const elems = this.displayDoc.body.children;
+        const elems = this.display.children;
         if (elems.length <= lineCount) return;
 
         const topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
         topElem.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth'});
     }
 
-    codeMirrorSetup() {
-        const cm = this.cm;
+    codeMirrorSetup(cm) {
         const context = this;
 
         // Text direction
         // cm.setOption('direction', this.textDirection);
         cm.setOption('direction', 'ltr'); // Will force to remain as ltr for now due to issues when HTML is in editor.
         // Custom key commands
-        let metaKey = code.getMetaKey();
+        let metaKey = this.Code.getMetaKey();
         const extraKeys = {};
         // Insert Image shortcut
         extraKeys[`${metaKey}-Alt-I`] = function(cm) {
@@ -209,20 +200,16 @@ class MarkdownEditor {
 
         // Handle image paste
         cm.on('paste', (cm, event) => {
-            const clipboardItems = event.clipboardData.items;
-            if (!event.clipboardData || !clipboardItems) return;
+            const clipboard = new Clipboard(event.clipboardData || event.dataTransfer);
 
-            // Don't handle if clipboard includes text content
-            for (let clipboardItem of clipboardItems) {
-                if (clipboardItem.type.includes('text/')) {
-                    return;
-                }
+            // Don't handle the event ourselves if no items exist of contains table-looking data
+            if (!clipboard.hasItems() || clipboard.containsTabularData()) {
+                return;
             }
 
-            for (let clipboardItem of clipboardItems) {
-                if (clipboardItem.type.includes("image")) {
-                    uploadImage(clipboardItem.getAsFile());
-                }
+            const images = clipboard.getImages();
+            for (const image of images) {
+                uploadImage(image);
             }
         });
 
@@ -240,13 +227,15 @@ class MarkdownEditor {
                 });
             }
 
-            if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) {
+            const clipboard = new Clipboard(event.dataTransfer);
+            if (clipboard.hasItems() && clipboard.getImages().length > 0) {
                 const cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
                 cm.setCursor(cursorPos);
                 event.stopPropagation();
                 event.preventDefault();
-                for (let i = 0; i < event.dataTransfer.files.length; i++) {
-                    uploadImage(event.dataTransfer.files[i]);
+                const images = clipboard.getImages();
+                for (const image of images) {
+                    uploadImage(image);
                 }
             }
 
@@ -292,7 +281,7 @@ class MarkdownEditor {
             let cursor = cm.getCursor();
             let lineContent = cm.getLine(cursor.line);
             let lineLen = lineContent.length;
-            let newLineContent = lineContent;
+            let newLineContent;
 
             if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
                 newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
@@ -308,9 +297,9 @@ class MarkdownEditor {
             let selection = cm.getSelection();
             if (selection === '') return wrapLine(start, end);
 
-            let newSelection = selection;
+            let newSelection;
             let frontDiff = 0;
-            let endDiff = 0;
+            let endDiff;
 
             if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
                 newSelection = selection.slice(start.length, selection.length - end.length);
@@ -356,7 +345,7 @@ class MarkdownEditor {
                 const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`;
                 replaceContent(placeHolderText, newContent);
             }).catch(err => {
-                window.$events.emit('error', trans('errors.image_upload_error'));
+                window.$events.emit('error', context.imageUploadErrorText);
                 replaceContent(placeHolderText, selectedText);
                 console.log(err);
             });
@@ -378,8 +367,9 @@ class MarkdownEditor {
     actionInsertImage() {
         const cursorPos = this.cm.getCursor('from');
         window.ImageManager.show(image => {
+            const imageUrl = image.thumbs.display || image.url;
             let selectedText = this.cm.getSelection();
-            let newText = "[![" + (selectedText || image.name) + "](" + image.thumbs.display + ")](" + image.url + ")";
+            let newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")";
             this.cm.focus();
             this.cm.replaceSelection(newText);
             this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
@@ -405,33 +395,38 @@ class MarkdownEditor {
         });
     }
 
+    getDrawioUrl() {
+        const drawioUrlElem = document.querySelector('[drawio-url]');
+        return drawioUrlElem ? drawioUrlElem.getAttribute('drawio-url') : false;
+    }
+
     // Show draw.io if enabled and handle save.
     actionStartDrawing() {
-        if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true') return;
-        let cursorPos = this.cm.getCursor('from');
+        const url = this.getDrawioUrl();
+        if (!url) return;
+
+        const cursorPos = this.cm.getCursor('from');
 
-        DrawIO.show(() => {
+        DrawIO.show(url,() => {
             return Promise.resolve('');
-        }, (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'))
+        }, (drawingData) => {
+
+            const data = {
+                image: drawingData,
+                uploaded_to: Number(this.pageId),
             };
 
-            window.$http.post(window.baseUrl('/images/drawio'), data).then(resp => {
+            window.$http.post("/images/drawio", data).then(resp => {
                 this.insertDrawing(resp.data, cursorPos);
                 DrawIO.close();
             }).catch(err => {
-                window.$events.emit('error', trans('errors.image_upload_error'));
-                console.log(err);
+                this.handleDrawingUploadError(err);
             });
         });
     }
 
     insertDrawing(image, originalCursor) {
-        const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
+        const newText = DrawIO.buildDrawingContentHtml(image);
         this.cm.focus();
         this.cm.replaceSelection(newText);
         this.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
@@ -439,42 +434,58 @@ class MarkdownEditor {
 
     // Show draw.io if enabled and handle save.
     actionEditDrawing(imgContainer) {
-        const drawingDisabled = document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true';
-        if (drawingDisabled) {
+        const drawioUrl = this.getDrawioUrl();
+        if (!drawioUrl) {
             return;
         }
 
         const cursorPos = this.cm.getCursor('from');
         const drawingId = imgContainer.getAttribute('drawio-diagram');
 
-        DrawIO.show(() => {
+        DrawIO.show(drawioUrl, () => {
             return DrawIO.load(drawingId);
-        }, (pngData) => {
+        }, (drawingData) => {
 
             let data = {
-                image: pngData,
-                uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
+                image: drawingData,
+                uploaded_to: Number(this.pageId),
             };
 
-            window.$http.post(window.baseUrl(`/images/drawio`), data).then(resp => {
-                let newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
-                let newContent = this.cm.getValue().split('\n').map(line => {
-                    if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
-                        return newText;
-                    }
-                    return line;
+            window.$http.post("/images/drawio", data).then(resp => {
+                const image = resp.data;
+                const newText = DrawIO.buildDrawingContentHtml(image);
+
+                const newContent = this.cm.getValue().split('\n').map(line => {
+                    const isDrawing = line.includes(`drawio-diagram="${drawingId}"`);
+                    return isDrawing ? newText : line;
                 }).join('\n');
+
                 this.cm.setValue(newContent);
                 this.cm.setCursor(cursorPos);
                 this.cm.focus();
                 DrawIO.close();
             }).catch(err => {
-                window.$events.emit('error', trans('errors.image_upload_error'));
-                console.log(err);
+                this.handleDrawingUploadError(err);
             });
         });
     }
 
+    handleDrawingUploadError(error) {
+        if (error.status === 413) {
+            window.$events.emit('error', this.serverUploadLimitText);
+        } else {
+            window.$events.emit('error', this.imageUploadErrorText);
+        }
+        console.log(error);
+    }
+
+    // Make the editor full screen
+    actionFullScreen() {
+        const alreadyFullscreen = this.elem.classList.contains('fullscreen');
+        this.elem.classList.toggle('fullscreen', !alreadyFullscreen);
+        document.body.classList.toggle('markdown-fullscreen', !alreadyFullscreen);
+    }
+
     // Scroll to a specified text
     scrollToText(searchText) {
         if (!searchText) {
@@ -532,6 +543,17 @@ class MarkdownEditor {
             const prependLineCount = markdown.split('\n').length;
             this.cm.setCursor(cursorPos.line + prependLineCount, cursorPos.ch);
         });
+
+        // Insert editor content at the current location
+        window.$events.listen('editor::insert', (eventContent) => {
+            const markdown = getContentToInsert(eventContent);
+            this.cm.replaceSelection(markdown);
+        });
+
+        // Focus on editor
+        window.$events.listen('editor::focus', () => {
+            this.cm.focus();
+        });
     }
 }