Move the CKEditor overlays into the page overlay container
authorAlexander Ebert <ebert@woltlab.com>
Tue, 8 Aug 2023 10:11:25 +0000 (12:11 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 8 Aug 2023 10:11:25 +0000 (12:11 +0200)
See https://www.woltlab.com/community/thread/300815-ckeditor-link-menu-non-functional-in-ckeditor-instances-in-a-dialog/
See ckeditor/ckeditor#14747

ts/WoltLabSuite/Core/Component/Ckeditor.ts
ts/WoltLabSuite/Core/Component/Ckeditor/Layer.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Ui/Dialog.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Layer.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dialog.js

index 458164b8347dc3fcd37079e18d3e1d93a4f42b3c..f83b2cee9474eb2091d11d4631ab59d61eccac1f 100644 (file)
@@ -26,6 +26,7 @@ import { element as scrollToElement } from "../Ui/Scroll";
 import Devtools from "../Devtools";
 import { ClassicEditor, CodeBlockConfig, EditorConfig, Element as CkeElement } from "./Ckeditor/Types";
 import { setupSubmitShortcut } from "./Ckeditor/Keyboard";
+import { setup as setupLayer } from "./Ckeditor/Layer";
 
 const instances = new WeakMap<HTMLElement, CKEditor>();
 
@@ -238,6 +239,8 @@ export async function setupCkeditor(
     throw new TypeError(`Cannot initialize the editor for '${element.id}' twice.`);
   }
 
+  setupLayer();
+
   const injectedStylesheet = injectCss();
 
   await Promise.all([
diff --git a/ts/WoltLabSuite/Core/Component/Ckeditor/Layer.ts b/ts/WoltLabSuite/Core/Component/Ckeditor/Layer.ts
new file mode 100644 (file)
index 0000000..77c7d9f
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Moves CKEditor’s overlay elements into the page overlay container. This
+ * preserves their functionality when the editor appears within a native
+ * `<dialog>` element.
+ * 
+ * See https://github.com/ckeditor/ckeditor5/issues/14747
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ * @woltlabExcludeBundle tiny
+ */
+
+import { getPageOverlayContainer } from "../../Helper/PageOverlay";
+
+const targetClassNames = ["ck-body-wrapper", "ck-inspector-wrapper", "ck-inspector-portal"];
+
+function setupWatcher(): void {
+  const observer = new MutationObserver((mutations) => {
+    for (const mutation of mutations) {
+      for (const node of mutation.addedNodes) {
+        if (!(node instanceof HTMLElement)) {
+          continue;
+        }
+
+        for (const className of targetClassNames) {
+          if (node.classList.contains(className)) {
+            getPageOverlayContainer().append(node);
+
+            continue;
+          }
+        }
+      }
+    }
+  });
+  observer.observe(document.body, {
+    childList: true,
+  });
+}
+
+let hasWatcher = false;
+export function setup(): void {
+  if (hasWatcher) {
+    return;
+  }
+
+  hasWatcher = true;
+  setupWatcher();
+}
index cf44de0966fe127cb9cd551395239c9d8677ef2f..f984816b71318a388de11c010a19941263bf4c7f 100644 (file)
@@ -25,6 +25,7 @@ import * as EventHandler from "../Event/Handler";
 import { AjaxCallbackSetup } from "../Ajax/Data";
 import CloseOverlay from "./CloseOverlay";
 import { createFocusTrap } from "focus-trap";
+import { adoptPageOverlayContainer, releasePageOverlayContainer } from "../Helper/PageOverlay";
 
 let _activeDialog: string | null = null;
 let _container: HTMLElement;
@@ -285,6 +286,8 @@ const UiDialog = {
       }, 200);
     }
 
+    adoptPageOverlayContainer(data.dialog);
+
     return data;
   },
 
@@ -811,6 +814,7 @@ const UiDialog = {
       }
     }
 
+    releasePageOverlayContainer(data.dialog);
     UiScreen.pageOverlayClose();
 
     if (_activeDialog === null) {
index 6475583d10e77fdd3a375162fcf73acb00c1b5fc..3725c0c5b741e904cfbb92a2a42ed4e7939de499 100644 (file)
@@ -12,7 +12,7 @@
  * @since 6.0
  * @woltlabExcludeBundle tiny
  */
-define(["require", "exports", "tslib", "./Ckeditor/Attachment", "./Ckeditor/Media", "./Ckeditor/Mention", "./Ckeditor/Quote", "./Ckeditor/Autosave", "./Ckeditor/Configuration", "./Ckeditor/Event", "./Ckeditor/SubmitOnEnter", "./Ckeditor/Normalizer", "../Ui/Scroll", "../Devtools", "./Ckeditor/Keyboard"], function (require, exports, tslib_1, Attachment_1, Media_1, Mention_1, Quote_1, Autosave_1, Configuration_1, Event_1, SubmitOnEnter_1, Normalizer_1, Scroll_1, Devtools_1, Keyboard_1) {
+define(["require", "exports", "tslib", "./Ckeditor/Attachment", "./Ckeditor/Media", "./Ckeditor/Mention", "./Ckeditor/Quote", "./Ckeditor/Autosave", "./Ckeditor/Configuration", "./Ckeditor/Event", "./Ckeditor/SubmitOnEnter", "./Ckeditor/Normalizer", "../Ui/Scroll", "../Devtools", "./Ckeditor/Keyboard", "./Ckeditor/Layer"], function (require, exports, tslib_1, Attachment_1, Media_1, Mention_1, Quote_1, Autosave_1, Configuration_1, Event_1, SubmitOnEnter_1, Normalizer_1, Scroll_1, Devtools_1, Keyboard_1, Layer_1) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.getCkeditorById = exports.getCkeditor = exports.setupCkeditor = void 0;
@@ -170,6 +170,7 @@ define(["require", "exports", "tslib", "./Ckeditor/Attachment", "./Ckeditor/Medi
         if (instances.has(element)) {
             throw new TypeError(`Cannot initialize the editor for '${element.id}' twice.`);
         }
+        (0, Layer_1.setup)();
         const injectedStylesheet = injectCss();
         await Promise.all([
             new Promise((resolve) => {
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Layer.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Layer.js
new file mode 100644 (file)
index 0000000..39206b2
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Moves CKEditor’s overlay elements into the page overlay container. This
+ * preserves their functionality when the editor appears within a native
+ * `<dialog>` element.
+ *
+ * See https://github.com/ckeditor/ckeditor5/issues/14747
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ * @woltlabExcludeBundle tiny
+ */
+define(["require", "exports", "../../Helper/PageOverlay"], function (require, exports, PageOverlay_1) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.setup = void 0;
+    const targetClassNames = ["ck-body-wrapper", "ck-inspector-wrapper", "ck-inspector-portal"];
+    function setupWatcher() {
+        const observer = new MutationObserver((mutations) => {
+            for (const mutation of mutations) {
+                for (const node of mutation.addedNodes) {
+                    if (!(node instanceof HTMLElement)) {
+                        continue;
+                    }
+                    for (const className of targetClassNames) {
+                        if (node.classList.contains(className)) {
+                            (0, PageOverlay_1.getPageOverlayContainer)().append(node);
+                            continue;
+                        }
+                    }
+                }
+            }
+        });
+        observer.observe(document.body, {
+            childList: true,
+        });
+    }
+    let hasWatcher = false;
+    function setup() {
+        if (hasWatcher) {
+            return;
+        }
+        hasWatcher = true;
+        setupWatcher();
+    }
+    exports.setup = setup;
+});
index 1d98ad52792023b63904c8faa5069dea300e910e..8cdd28a9b4a653e28b181bed70e1086a54854a48 100644 (file)
@@ -6,7 +6,7 @@
  * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @deprecated 6.0 Use `dialogFactory()` instead.
  */
-define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "./Screen", "../Dom/Util", "../Language", "../Environment", "../Event/Handler", "./CloseOverlay", "focus-trap"], function (require, exports, tslib_1, Core, Listener_1, UiScreen, Util_1, Language, Environment, EventHandler, CloseOverlay_1, focus_trap_1) {
+define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "./Screen", "../Dom/Util", "../Language", "../Environment", "../Event/Handler", "./CloseOverlay", "focus-trap", "../Helper/PageOverlay"], function (require, exports, tslib_1, Core, Listener_1, UiScreen, Util_1, Language, Environment, EventHandler, CloseOverlay_1, focus_trap_1, PageOverlay_1) {
     "use strict";
     Core = tslib_1.__importStar(Core);
     Listener_1 = tslib_1.__importDefault(Listener_1);
@@ -234,6 +234,7 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "./S
                     data.content.querySelector("input, textarea")?.focus();
                 }, 200);
             }
+            (0, PageOverlay_1.adoptPageOverlayContainer)(data.dialog);
             return data;
         },
         /**
@@ -668,6 +669,7 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "./S
                     break;
                 }
             }
+            (0, PageOverlay_1.releasePageOverlayContainer)(data.dialog);
             UiScreen.pageOverlayClose();
             if (_activeDialog === null) {
                 _container.setAttribute("aria-hidden", "true");