Clean up the markup passed to CKEditor
authorAlexander Ebert <ebert@woltlab.com>
Thu, 4 May 2023 17:44:35 +0000 (19:44 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 8 May 2023 16:34:04 +0000 (18:34 +0200)
ts/WoltLabSuite/Core/Component/Ckeditor/Cleanup.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Cleanup.js

index b5b0d5adbb37a36a2cbfae7c5c4f67215d2c6106..88edda2f39b8331ba494e1e967201bf8c074ffd7 100644 (file)
@@ -1,3 +1,74 @@
+/**
+ * Cleans up the markup of legacy messages.
+ *
+ * Messages created in the previous editor used empty paragraphs to create empty
+ * lines. In addition, Firefox kept trailing <br> in lines with content, which
+ * causes issues with CKEditor.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+
+import DomUtil from "../../Dom/Util";
+
+function unwrapBr(div: HTMLElement): void {
+  div.querySelectorAll("br").forEach((br) => {
+    if (br.previousSibling || br.nextSibling) {
+      return;
+    }
+
+    let parent: HTMLElement | null = br;
+    while ((parent = parent.parentElement) !== null) {
+      switch (parent.tagName) {
+        case "B":
+        case "EM":
+        case "I":
+        case "STRONG":
+        case "SUB":
+        case "SUP":
+        case "SPAN":
+        case "U":
+          if (br.previousSibling || br.nextSibling) {
+            return;
+          }
+
+          parent.insertAdjacentElement("afterend", br);
+          parent.remove();
+          parent = br;
+          break;
+
+        default:
+          return;
+      }
+    }
+  });
+}
+
+function removeTrailingBr(div: HTMLElement): void {
+  div.querySelectorAll("br").forEach((br) => {
+    if (br.dataset.ckeFiller === "true") {
+      return;
+    }
+
+    const paragraph = br.closest("p");
+    if (paragraph === null) {
+      return;
+    }
+
+    if (!DomUtil.isAtNodeEnd(br, paragraph)) {
+      return;
+    }
+
+    if (paragraph.innerHTML === "<br>") {
+      paragraph.remove();
+    } else {
+      br.remove();
+    }
+  });
+}
+
 function stripLegacySpacerParagraphs(div: HTMLElement): void {
   div.querySelectorAll("p").forEach((paragraph) => {
     if (paragraph.childElementCount === 1) {
@@ -19,6 +90,8 @@ export function normalizeLegacyMessage(element: HTMLElement): void {
   const div = document.createElement("div");
   div.innerHTML = element.value;
 
+  unwrapBr(div);
+  removeTrailingBr(div);
   stripLegacySpacerParagraphs(div);
 
   element.value = div.innerHTML;
index 31fa4715f3c5fe6f7613c754cfbb1096b099dcd3..a40ec4aa1fb44525e16d4080e9f1505f9eb38a94 100644 (file)
@@ -1,7 +1,69 @@
-define(["require", "exports"], function (require, exports) {
+/**
+ * Cleans up the markup of legacy messages.
+ *
+ * Messages created in the previous editor used empty paragraphs to create empty
+ * lines. In addition, Firefox kept trailing <br> in lines with content, which
+ * causes issues with CKEditor.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+define(["require", "exports", "tslib", "../../Dom/Util"], function (require, exports, tslib_1, Util_1) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.normalizeLegacyMessage = void 0;
+    Util_1 = tslib_1.__importDefault(Util_1);
+    function unwrapBr(div) {
+        div.querySelectorAll("br").forEach((br) => {
+            if (br.previousSibling || br.nextSibling) {
+                return;
+            }
+            let parent = br;
+            while ((parent = parent.parentElement) !== null) {
+                switch (parent.tagName) {
+                    case "B":
+                    case "EM":
+                    case "I":
+                    case "STRONG":
+                    case "SUB":
+                    case "SUP":
+                    case "SPAN":
+                    case "U":
+                        if (br.previousSibling || br.nextSibling) {
+                            return;
+                        }
+                        parent.insertAdjacentElement("afterend", br);
+                        parent.remove();
+                        parent = br;
+                        break;
+                    default:
+                        return;
+                }
+            }
+        });
+    }
+    function removeTrailingBr(div) {
+        div.querySelectorAll("br").forEach((br) => {
+            if (br.dataset.ckeFiller === "true") {
+                return;
+            }
+            const paragraph = br.closest("p");
+            if (paragraph === null) {
+                return;
+            }
+            if (!Util_1.default.isAtNodeEnd(br, paragraph)) {
+                return;
+            }
+            if (paragraph.innerHTML === "<br>") {
+                paragraph.remove();
+            }
+            else {
+                br.remove();
+            }
+        });
+    }
     function stripLegacySpacerParagraphs(div) {
         div.querySelectorAll("p").forEach((paragraph) => {
             if (paragraph.childElementCount === 1) {
@@ -20,6 +82,8 @@ define(["require", "exports"], function (require, exports) {
         }
         const div = document.createElement("div");
         div.innerHTML = element.value;
+        unwrapBr(div);
+        removeTrailingBr(div);
         stripLegacySpacerParagraphs(div);
         element.value = div.innerHTML;
     }