From a5b512fd8870a15c173c5c25280e5c79ebc9da5b Mon Sep 17 00:00:00 2001
From: Oliver Hader <oliver@typo3.org>
Date: Sat, 18 Apr 2020 22:34:36 +0200
Subject: [PATCH] [FEATURE] Introduce DocumentService as JQuery.ready
 substitute

Module TYPO3/CMS/Core/DocumentService provides native JavaScript
functions to detect DOM ready-state returning a Promise<Document>.

`$(document).ready(() => {...});` can be replaced by
`documentService.ready().then(() => {...});`

Resolves: #91122
Releases: master
Change-Id: Id812f786430f1ced6265493dd0bae472b8144588
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64241
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
---
 .../Public/TypeScript/GlobalEventHandler.ts   | 20 +------
 .../Public/TypeScript/DocumentService.ts      | 59 +++++++++++++++++++
 .../Public/JavaScript/GlobalEventHandler.js   |  2 +-
 ...DocumentServiceAsJQueryreadySubstitute.rst | 40 +++++++++++++
 .../Public/JavaScript/DocumentService.js      | 13 ++++
 5 files changed, 116 insertions(+), 18 deletions(-)
 create mode 100644 Build/Sources/TypeScript/core/Resources/Public/TypeScript/DocumentService.ts
 create mode 100644 typo3/sysext/core/Documentation/Changelog/10.4/Feature-91122-IntroduceDocumentServiceAsJQueryreadySubstitute.rst
 create mode 100644 typo3/sysext/core/Resources/Public/JavaScript/DocumentService.js

diff --git a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/GlobalEventHandler.ts b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/GlobalEventHandler.ts
index 86d12100ea82..aae1d5e39f93 100644
--- a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/GlobalEventHandler.ts
+++ b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/GlobalEventHandler.ts
@@ -11,6 +11,8 @@
  * The TYPO3 project - inspiring people to share!
  */
 
+import documentService = require('TYPO3/CMS/Core/DocumentService');
+
 type HTMLFormChildElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
 
 /**
@@ -37,25 +39,9 @@ class GlobalEventHandler {
   };
 
   constructor() {
-    this.onReady(() => {
-      this.registerEvents();
-    });
+    documentService.ready().then((): void => this.registerEvents());
   };
 
-  private onReady(callback: Function): void {
-    if (document.readyState === 'complete') {
-      callback.call(this);
-    } else {
-      const delegate = () => {
-        window.removeEventListener('load', delegate);
-        document.removeEventListener('DOMContentLoaded', delegate);
-        callback.call(this);
-      };
-      window.addEventListener('load', delegate);
-      document.addEventListener('DOMContentLoaded', delegate);
-    }
-  }
-
   private registerEvents(): void {
     document.querySelectorAll(this.options.onChangeSelector).forEach((element: HTMLElement) => {
       document.addEventListener('change', this.handleChangeEvent.bind(this));
diff --git a/Build/Sources/TypeScript/core/Resources/Public/TypeScript/DocumentService.ts b/Build/Sources/TypeScript/core/Resources/Public/TypeScript/DocumentService.ts
new file mode 100644
index 000000000000..694c794fac82
--- /dev/null
+++ b/Build/Sources/TypeScript/core/Resources/Public/TypeScript/DocumentService.ts
@@ -0,0 +1,59 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Module: TYPO3/CMS/Core/DocumentService
+ * @exports TYPO3/CMS/Core/DocumentService
+ */
+class DocumentService {
+  private readonly windowRef: Window;
+  private readonly documentRef: Document;
+
+  /**
+   * @param {Window} windowRef
+   * @param {Document} documentRef
+   */
+  constructor(windowRef: Window = window, documentRef: Document = document) {
+    this.windowRef = windowRef;
+    this.documentRef = documentRef;
+  }
+
+  ready(): Promise<Document> {
+    return new Promise<Document>((resolve: Function, reject: Function) => {
+      if (this.documentRef.readyState === 'complete') {
+        resolve(this.documentRef);
+      } else {
+        // timeout & reject after 30 seconds
+        const timer = setTimeout((): void => {
+          clearListeners();
+          reject(this.documentRef);
+        }, 30000);
+        const clearListeners = (): void => {
+          this.windowRef.removeEventListener('load', delegate);
+          this.documentRef.removeEventListener('DOMContentLoaded', delegate);
+        };
+        const delegate = (): void => {
+          clearListeners();
+          clearTimeout(timer);
+          resolve(this.documentRef);
+        };
+        this.windowRef.addEventListener('load', delegate);
+        this.documentRef.addEventListener('DOMContentLoaded', delegate);
+      }
+
+    });
+  }
+}
+
+const documentService = new DocumentService();
+export = documentService;
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/GlobalEventHandler.js b/typo3/sysext/backend/Resources/Public/JavaScript/GlobalEventHandler.js
index 2bf7e9f1e76a..8a14ce2355d8 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/GlobalEventHandler.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/GlobalEventHandler.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-define(["require","exports"],(function(e,t){"use strict";return new class{constructor(){this.options={onChangeSelector:'[data-global-event="change"]',onClickSelector:'[data-global-event="click"]'},this.onReady(()=>{this.registerEvents()})}onReady(e){if("complete"===document.readyState)e.call(this);else{const t=()=>{window.removeEventListener("load",t),document.removeEventListener("DOMContentLoaded",t),e.call(this)};window.addEventListener("load",t),document.addEventListener("DOMContentLoaded",t)}}registerEvents(){document.querySelectorAll(this.options.onChangeSelector).forEach(e=>{document.addEventListener("change",this.handleChangeEvent.bind(this))}),document.querySelectorAll(this.options.onClickSelector).forEach(e=>{document.addEventListener("click",this.handleClickEvent.bind(this))})}handleChangeEvent(e){const t=e.target;this.handleSubmitAction(e,t)||this.handleNavigateAction(e,t)}handleClickEvent(e){e.currentTarget}handleSubmitAction(e,t){const n=t.dataset.actionSubmit;if(!n)return!1;if("$form"===n&&this.isHTMLFormChildElement(t))return t.form.submit(),!0;const o=document.querySelector(n);return o instanceof HTMLFormElement&&(o.submit(),!0)}handleNavigateAction(e,t){const n=t.dataset.actionNavigate;if(!n)return!1;const o=this.resolveHTMLFormChildElementValue(t);return!("$value"!==n||!o)&&(window.location.href=o,!0)}isHTMLFormChildElement(e){return e instanceof HTMLSelectElement||e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement}resolveHTMLFormChildElementValue(e){return e instanceof HTMLSelectElement?e.options[e.selectedIndex].value:e instanceof HTMLInputElement?e.value:null}}}));
\ No newline at end of file
+define(["require","exports","TYPO3/CMS/Core/DocumentService"],(function(e,t,n){"use strict";return new class{constructor(){this.options={onChangeSelector:'[data-global-event="change"]',onClickSelector:'[data-global-event="click"]'},n.ready().then(()=>this.registerEvents())}registerEvents(){document.querySelectorAll(this.options.onChangeSelector).forEach(e=>{document.addEventListener("change",this.handleChangeEvent.bind(this))}),document.querySelectorAll(this.options.onClickSelector).forEach(e=>{document.addEventListener("click",this.handleClickEvent.bind(this))})}handleChangeEvent(e){const t=e.target;this.handleSubmitAction(e,t)||this.handleNavigateAction(e,t)}handleClickEvent(e){e.currentTarget}handleSubmitAction(e,t){const n=t.dataset.actionSubmit;if(!n)return!1;if("$form"===n&&this.isHTMLFormChildElement(t))return t.form.submit(),!0;const i=document.querySelector(n);return i instanceof HTMLFormElement&&(i.submit(),!0)}handleNavigateAction(e,t){const n=t.dataset.actionNavigate;if(!n)return!1;const i=this.resolveHTMLFormChildElementValue(t);return!("$value"!==n||!i)&&(window.location.href=i,!0)}isHTMLFormChildElement(e){return e instanceof HTMLSelectElement||e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement}resolveHTMLFormChildElementValue(e){return e instanceof HTMLSelectElement?e.options[e.selectedIndex].value:e instanceof HTMLInputElement?e.value:null}}}));
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/10.4/Feature-91122-IntroduceDocumentServiceAsJQueryreadySubstitute.rst b/typo3/sysext/core/Documentation/Changelog/10.4/Feature-91122-IntroduceDocumentServiceAsJQueryreadySubstitute.rst
new file mode 100644
index 000000000000..cb22c298dfd8
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/10.4/Feature-91122-IntroduceDocumentServiceAsJQueryreadySubstitute.rst
@@ -0,0 +1,40 @@
+.. include:: ../../Includes.txt
+
+======================================================================
+Feature: #91122 - Introduce DocumentService as JQuery.ready substitute
+======================================================================
+
+See :issue:`91122`
+
+Description
+===========
+
+The module :js:`TYPO3/CMS/Core/DocumentService` provides native JavaScript
+functions to detect DOM ready-state returning a :js:`Promise<Document>`.
+
+Internally the Promise is resolved when native :js:`DOMContentLoaded` event has
+been emitted or when :js:`document.readyState` is defined already. It means
+that initial HTML document has been completely loaded and parsed, without
+waiting for stylesheets, images, and subframes to finish loading.
+
+
+Impact
+======
+
+.. code-block:: javascript
+
+   $(document).ready(() => {
+     // your application code
+   });
+
+Above JQuery code can be transformed into the following using :js:`DocumentService`:
+
+.. code-block:: javascript
+
+   require(['TYPO3/CMS/Core/DocumentService'], function (DocumentService) {
+     DocumentService.ready().then(() => {
+       // your application code
+     });
+   });
+
+.. index:: Backend, JavaScript, ext:core
diff --git a/typo3/sysext/core/Resources/Public/JavaScript/DocumentService.js b/typo3/sysext/core/Resources/Public/JavaScript/DocumentService.js
new file mode 100644
index 000000000000..1a5be706fbe1
--- /dev/null
+++ b/typo3/sysext/core/Resources/Public/JavaScript/DocumentService.js
@@ -0,0 +1,13 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+define(["require","exports"],(function(e,t){"use strict";return new class{constructor(e=window,t=document){this.windowRef=e,this.documentRef=t}ready(){return new Promise((e,t)=>{if("complete"===this.documentRef.readyState)e(this.documentRef);else{const n=setTimeout(()=>{o(),t(this.documentRef)},3e4),o=()=>{this.windowRef.removeEventListener("load",i),this.documentRef.removeEventListener("DOMContentLoaded",i)},i=()=>{o(),clearTimeout(n),e(this.documentRef)};this.windowRef.addEventListener("load",i),this.documentRef.addEventListener("DOMContentLoaded",i)}})}}}));
\ No newline at end of file
-- 
GitLab