From 21ec90dd43428e9d8bc5263a0e605a20d3794384 Mon Sep 17 00:00:00 2001
From: Andreas Fernandez <a.fernandez@scripting-base.de>
Date: Sat, 29 Feb 2020 12:40:36 +0100
Subject: [PATCH] [BUGFIX] Initialize CodeMirror if element becomes visible to
 client
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

CodeMirror initialization fails if its textarea is not rendered yet. To
fix this issue, an Intersection Observer is installed and initializes the
CodeMirror instance when the element becomes visibile to the client.

Resolves: #90525
Releases: master, 9.5
Change-Id: I93247a41827547abec5369e5904ec4dd2354eb7e
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63504
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Christian Eßl <indy.essl@gmail.com>
Tested-by: Riccardo De Contardi <erredeco@gmail.com>
Tested-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Christian Eßl <indy.essl@gmail.com>
Reviewed-by: Daniel Goerz <daniel.goerz@posteo.de>
---
 .../Resources/Public/TypeScript/T3editor.ts   | 131 ++++++++++--------
 .../Classes/Form/Element/T3editorElement.php  |   2 +-
 .../Resources/Public/JavaScript/T3editor.js   |   2 +-
 3 files changed, 75 insertions(+), 60 deletions(-)

diff --git a/Build/Sources/TypeScript/t3editor/Resources/Public/TypeScript/T3editor.ts b/Build/Sources/TypeScript/t3editor/Resources/Public/TypeScript/T3editor.ts
index 19c616b0278f..c7d61962eb07 100644
--- a/Build/Sources/TypeScript/t3editor/Resources/Public/TypeScript/T3editor.ts
+++ b/Build/Sources/TypeScript/t3editor/Resources/Public/TypeScript/T3editor.ts
@@ -46,72 +46,87 @@ class T3editor {
   }
 
   /**
-   * Initializes CodeMirror on available texteditors
+   * Initialize the events
    */
-  public findAndInitializeEditors(): void {
-    $(document).find('textarea.t3editor').each(function(this: Element): void {
-      const $textarea = $(this);
-
-      if (!$textarea.prop('is_t3editor')) {
-        const config = $textarea.data('codemirror-config');
-        const modeParts = config.mode.split('/');
-        const addons = $.merge([modeParts.join('/')], JSON.parse(config.addons));
-        const options = JSON.parse(config.options);
-
-        // load mode + registered addons
-        require(addons, (): void => {
-          const cm = CodeMirror.fromTextArea($textarea.get(0), {
-            extraKeys: {
-              'Ctrl-F': 'findPersistent',
-              'Cmd-F': 'findPersistent',
-              'Ctrl-Alt-F': (codemirror: any): void => {
-                codemirror.setOption('fullScreen', !codemirror.getOption('fullScreen'));
-              },
-              'Ctrl-Space': 'autocomplete',
-              'Esc': (codemirror: any): void => {
-                if (codemirror.getOption('fullScreen')) {
-                  codemirror.setOption('fullScreen', false);
-                }
-              },
-            },
-            fullScreen: false,
-            lineNumbers: true,
-            lineWrapping: true,
-            mode: modeParts[modeParts.length - 1],
-          });
-
-          // set options
-          $.each(options, (key: string, value: any): void => {
-            cm.setOption(key, value);
-          });
-
-          // Mark form as changed if code editor content has changed
-          cm.on('change', (): void => {
-            FormEngine.Validation.markFieldAsChanged($textarea);
-          });
-
-          cm.addPanel(
-            T3editor.createPanelNode('bottom', $textarea.attr('alt')),
-            {
-              position: 'bottom',
-              stable: true,
-            },
-          );
-        });
-
-        $textarea.prop('is_t3editor', true);
-      }
+  public initialize(): void {
+    $((): void => {
+      this.observeEditorCandidates();
     });
   }
 
   /**
-   * Initialize the events
+   * Initializes CodeMirror on available texteditors
    */
-  public initialize(): void {
-    $((): void => {
-      this.findAndInitializeEditors();
+  public observeEditorCandidates(): void {
+    const observerOptions = {
+      root: document.body
+    };
+
+    let observer = new IntersectionObserver((entries: IntersectionObserverEntry[]): void => {
+      entries.forEach((entry: IntersectionObserverEntry): void => {
+        if (entry.intersectionRatio > 0) {
+          const $target = $(entry.target);
+          if (!$target.prop('is_t3editor')) {
+            this.initializeEditor($target);
+          }
+        }
+      })
+    }, observerOptions);
+
+    document.querySelectorAll('textarea.t3editor').forEach((textarea: HTMLTextAreaElement): void => {
+      observer.observe(textarea);
     });
   }
+
+  private initializeEditor($textarea: JQuery): void {
+    const config = $textarea.data('codemirror-config');
+    const modeParts = config.mode.split('/');
+    const addons = $.merge([modeParts.join('/')], JSON.parse(config.addons));
+    const options = JSON.parse(config.options);
+
+    // load mode + registered addons
+    require(addons, (): void => {
+      const cm = CodeMirror.fromTextArea($textarea.get(0), {
+        extraKeys: {
+          'Ctrl-F': 'findPersistent',
+          'Cmd-F': 'findPersistent',
+          'Ctrl-Alt-F': (codemirror: any): void => {
+            codemirror.setOption('fullScreen', !codemirror.getOption('fullScreen'));
+          },
+          'Ctrl-Space': 'autocomplete',
+          'Esc': (codemirror: any): void => {
+            if (codemirror.getOption('fullScreen')) {
+              codemirror.setOption('fullScreen', false);
+            }
+          },
+        },
+        fullScreen: false,
+        lineNumbers: true,
+        lineWrapping: true,
+        mode: modeParts[modeParts.length - 1],
+      });
+
+      // set options
+      $.each(options, (key: string, value: any): void => {
+        cm.setOption(key, value);
+      });
+
+      // Mark form as changed if code editor content has changed
+      cm.on('change', (): void => {
+        FormEngine.Validation.markFieldAsChanged($textarea);
+      });
+
+      cm.addPanel(
+        T3editor.createPanelNode('bottom', $textarea.attr('alt')),
+        {
+          position: 'bottom',
+          stable: true,
+        },
+      );
+    });
+
+    $textarea.prop('is_t3editor', true);
+  }
 }
 
 // create an instance and return it
diff --git a/typo3/sysext/t3editor/Classes/Form/Element/T3editorElement.php b/typo3/sysext/t3editor/Classes/Form/Element/T3editorElement.php
index 8f23804b2835..9462abcc9dbd 100644
--- a/typo3/sysext/t3editor/Classes/Form/Element/T3editorElement.php
+++ b/typo3/sysext/t3editor/Classes/Form/Element/T3editorElement.php
@@ -100,7 +100,7 @@ class T3editorElement extends AbstractFormElement
         $this->resultArray['stylesheetFiles'][] = $codeMirrorPath . '/lib/codemirror.css';
         $this->resultArray['stylesheetFiles'][] = $this->extPath . '/Resources/Public/Css/t3editor.css';
         $this->resultArray['requireJsModules'][] = [
-            'TYPO3/CMS/T3editor/T3editor' => 'function(T3editor) {T3editor.findAndInitializeEditors()}'
+            'TYPO3/CMS/T3editor/T3editor' => 'function(T3editor) {T3editor.observeEditorCandidates()}'
         ];
 
         // Compile and register t3editor configuration
diff --git a/typo3/sysext/t3editor/Resources/Public/JavaScript/T3editor.js b/typo3/sysext/t3editor/Resources/Public/JavaScript/T3editor.js
index 109676b53979..61e0438d0a59 100644
--- a/typo3/sysext/t3editor/Resources/Public/JavaScript/T3editor.js
+++ b/typo3/sysext/t3editor/Resources/Public/JavaScript/T3editor.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-define(["require","exports","cm/lib/codemirror","jquery","TYPO3/CMS/Backend/FormEngine"],(function(e,t,i,n,r){"use strict";class o{static createPanelNode(e,t){return n("<div />",{class:"CodeMirror-panel CodeMirror-panel-"+e,id:"panel-"+e}).append(n("<span />").text(t)).get(0)}constructor(){this.initialize()}findAndInitializeEditors(){n(document).find("textarea.t3editor").each((function(){const t=n(this);if(!t.prop("is_t3editor")){const a=t.data("codemirror-config"),s=a.mode.split("/"),l=n.merge([s.join("/")],JSON.parse(a.addons)),d=JSON.parse(a.options);e(l,()=>{const e=i.fromTextArea(t.get(0),{extraKeys:{"Ctrl-F":"findPersistent","Cmd-F":"findPersistent","Ctrl-Alt-F":e=>{e.setOption("fullScreen",!e.getOption("fullScreen"))},"Ctrl-Space":"autocomplete",Esc:e=>{e.getOption("fullScreen")&&e.setOption("fullScreen",!1)}},fullScreen:!1,lineNumbers:!0,lineWrapping:!0,mode:s[s.length-1]});n.each(d,(t,i)=>{e.setOption(t,i)}),e.on("change",()=>{r.Validation.markFieldAsChanged(t)}),e.addPanel(o.createPanelNode("bottom",t.attr("alt")),{position:"bottom",stable:!0})}),t.prop("is_t3editor",!0)}}))}initialize(){n(()=>{this.findAndInitializeEditors()})}}return new o}));
\ No newline at end of file
+define(["require","exports","cm/lib/codemirror","jquery","TYPO3/CMS/Backend/FormEngine"],(function(e,t,i,r,o){"use strict";class n{static createPanelNode(e,t){return r("<div />",{class:"CodeMirror-panel CodeMirror-panel-"+e,id:"panel-"+e}).append(r("<span />").text(t)).get(0)}constructor(){this.initialize()}initialize(){r(()=>{this.observeEditorCandidates()})}observeEditorCandidates(){const e={root:document.body};let t=new IntersectionObserver(e=>{e.forEach(e=>{if(e.intersectionRatio>0){const t=r(e.target);t.prop("is_t3editor")||this.initializeEditor(t)}})},e);document.querySelectorAll("textarea.t3editor").forEach(e=>{t.observe(e)})}initializeEditor(t){const a=t.data("codemirror-config"),s=a.mode.split("/"),l=r.merge([s.join("/")],JSON.parse(a.addons)),d=JSON.parse(a.options);e(l,()=>{const e=i.fromTextArea(t.get(0),{extraKeys:{"Ctrl-F":"findPersistent","Cmd-F":"findPersistent","Ctrl-Alt-F":e=>{e.setOption("fullScreen",!e.getOption("fullScreen"))},"Ctrl-Space":"autocomplete",Esc:e=>{e.getOption("fullScreen")&&e.setOption("fullScreen",!1)}},fullScreen:!1,lineNumbers:!0,lineWrapping:!0,mode:s[s.length-1]});r.each(d,(t,i)=>{e.setOption(t,i)}),e.on("change",()=>{o.Validation.markFieldAsChanged(t)}),e.addPanel(n.createPanelNode("bottom",t.attr("alt")),{position:"bottom",stable:!0})}),t.prop("is_t3editor",!0)}}return new n}));
\ No newline at end of file
-- 
GitLab