From 6545afacd69bbb1d13e4e4c97da84004789a99e8 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Thu, 23 Feb 2023 12:30:27 +0000
Subject: [PATCH] Changed autosave handling for better editor performance

This changes how the editors interact with the parent page-editor
compontent, which handles auto-saving.
Instead of blasting the full editor content upon any change to that
parent compontent, the editors just alert of a change, without the
content. The parent compontent then requests the editor content from the
editor component when it needs that data for an autosave.

For #3981
---
 resources/js/components/markdown-editor.js |  9 ++++
 resources/js/components/page-editor.js     | 54 +++++++++++-----------
 resources/js/components/wysiwyg-editor.js  | 15 +++++-
 resources/js/markdown/actions.js           | 14 +++++-
 resources/js/wysiwyg/config.js             |  3 +-
 5 files changed, 64 insertions(+), 31 deletions(-)

diff --git a/resources/js/components/markdown-editor.js b/resources/js/components/markdown-editor.js
index 4c3de91f6..5cd92cae2 100644
--- a/resources/js/components/markdown-editor.js
+++ b/resources/js/components/markdown-editor.js
@@ -137,4 +137,13 @@ export class MarkdownEditor extends Component {
         return drawioAttrEl.getAttribute('drawio-url') || '';
     }
 
+    /**
+     * Get the content of this editor.
+     * Used by the parent page editor component.
+     * @return {{html: String, markdown: String}}
+     */
+    getContent() {
+        return this.editor.actions.getContent();
+    }
+
 }
diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js
index 950a5a3b3..c58f45b66 100644
--- a/resources/js/components/page-editor.js
+++ b/resources/js/components/page-editor.js
@@ -33,12 +33,11 @@ export class PageEditor extends Component {
         this.setChangelogText = this.$opts.setChangelogText;
 
         // State data
-        this.editorHTML = '';
-        this.editorMarkdown = '';
         this.autoSave = {
             interval: null,
             frequency: 30000,
             last: 0,
+            pendingChange: false,
         };
         this.shownWarningsCache = new Set();
 
@@ -59,12 +58,12 @@ export class PageEditor extends Component {
         window.$events.listen('editor-save-page', this.savePage.bind(this));
 
         // Listen to content changes from the editor
-        window.$events.listen('editor-html-change', html => {
-            this.editorHTML = html;
-        });
-        window.$events.listen('editor-markdown-change', markdown => {
-            this.editorMarkdown = markdown;
-        });
+        const onContentChange = () => this.autoSave.pendingChange = true;
+        window.$events.listen('editor-html-change', onContentChange);
+        window.$events.listen('editor-markdown-change', onContentChange);
+
+        // Listen to changes on the title input
+        this.titleElem.addEventListener('input', onContentChange);
 
         // Changelog controls
         const updateChangelogDebounced = debounce(this.updateChangelogDisplay.bind(this), 300, false);
@@ -89,18 +88,17 @@ export class PageEditor extends Component {
     }
 
     startAutoSave() {
-        let lastContent = this.titleElem.value.trim() + '::' + this.editorHTML;
-        this.autoSaveInterval = window.setInterval(() => {
-            // Stop if manually saved recently to prevent bombarding the server
-            let savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency)/2);
-            if (savedRecently) return;
-            const newContent = this.titleElem.value.trim() + '::' + this.editorHTML;
-            if (newContent !== lastContent) {
-                lastContent = newContent;
-                this.saveDraft();
-            }
+        this.autoSave.interval = window.setInterval(this.runAutoSave.bind(this), this.autoSave.frequency);
+    }
 
-        }, this.autoSave.frequency);
+    runAutoSave() {
+        // Stop if manually saved recently to prevent bombarding the server
+        const savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency)/2);
+        if (savedRecently || !this.autoSave.pendingChange) {
+            return;
+        }
+
+        this.saveDraft()
     }
 
     savePage() {
@@ -108,14 +106,10 @@ export class PageEditor extends Component {
     }
 
     async saveDraft() {
-        const data = {
-            name: this.titleElem.value.trim(),
-            html: this.editorHTML,
-        };
+        const data = {name: this.titleElem.value.trim()};
 
-        if (this.editorType === 'markdown') {
-            data.markdown = this.editorMarkdown;
-        }
+        const editorContent = this.getEditorComponent().getContent();
+        Object.assign(data, editorContent);
 
         let didSave = false;
         try {
@@ -132,6 +126,7 @@ export class PageEditor extends Component {
             }
 
             didSave = true;
+            this.autoSave.pendingChange = false;
         } catch (err) {
             // Save the editor content in LocalStorage as a last resort, just in case.
             try {
@@ -207,4 +202,11 @@ export class PageEditor extends Component {
         }
     }
 
+    /**
+     * @return MarkdownEditor|WysiwygEditor
+     */
+    getEditorComponent() {
+        return window.$components.first('markdown-editor') || window.$components.first('wysiwyg-editor');
+    }
+
 }
diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js
index 976dba68f..96731a0d9 100644
--- a/resources/js/components/wysiwyg-editor.js
+++ b/resources/js/components/wysiwyg-editor.js
@@ -25,7 +25,9 @@ export class WysiwygEditor extends Component {
         });
 
         window.$events.emitPublic(this.elem, 'editor-tinymce::pre-init', {config: this.tinyMceConfig});
-        window.tinymce.init(this.tinyMceConfig);
+        window.tinymce.init(this.tinyMceConfig).then(editors => {
+            this.editor = editors[0];
+        });
     }
 
     getDrawIoUrl() {
@@ -36,4 +38,15 @@ export class WysiwygEditor extends Component {
         return '';
     }
 
+    /**
+     * Get the content of this editor.
+     * Used by the parent page editor component.
+     * @return {{html: String}}
+     */
+    getContent() {
+        return {
+            html: this.editor.getContent()
+        };
+    }
+
 }
\ No newline at end of file
diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js
index c2c3409a5..9faf43de3 100644
--- a/resources/js/markdown/actions.js
+++ b/resources/js/markdown/actions.js
@@ -6,6 +6,10 @@ export class Actions {
      */
     constructor(editor) {
         this.editor = editor;
+        this.lastContent = {
+            html: '',
+            markdown: '',
+        };
     }
 
     updateAndRender() {
@@ -13,11 +17,17 @@ export class Actions {
         this.editor.config.inputEl.value = content;
 
         const html = this.editor.markdown.render(content);
-        window.$events.emit('editor-html-change', html);
-        window.$events.emit('editor-markdown-change', content);
+        window.$events.emit('editor-html-change', '');
+        window.$events.emit('editor-markdown-change', '');
+        this.lastContent.html = html;
+        this.lastContent.markdown = content;
         this.editor.display.patchWithHtml(html);
     }
 
+    getContent() {
+        return this.lastContent;
+    }
+
     insertImage() {
         const cursorPos = this.editor.cm.getCursor('from');
         /** @type {ImageManager} **/
diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js
index d5ec20e26..85c1919d4 100644
--- a/resources/js/wysiwyg/config.js
+++ b/resources/js/wysiwyg/config.js
@@ -185,11 +185,10 @@ function getSetupCallback(options) {
         });
 
         function editorChange() {
-            const content = editor.getContent();
             if (options.darkMode) {
                 editor.contentDocument.documentElement.classList.add('dark-mode');
             }
-            window.$events.emit('editor-html-change', content);
+            window.$events.emit('editor-html-change', '');
         }
 
         // Custom handler hook