From 52911813391d73859241170e5a03bd1d55efb6d4 Mon Sep 17 00:00:00 2001
From: nod_ <nod_@598310.no-reply.drupal.org>
Date: Thu, 3 Feb 2022 01:55:16 +0000
Subject: [PATCH] Issue #3248469 by nod_, lauriii, Wim Leers, longwave:
 Research if the CKE off-canvas CSS reset could be optimized

---
 .../modules/ckeditor5/ckeditor5.libraries.yml |   1 +
 core/modules/ckeditor5/js/ckeditor5.es6.js    | 175 +++++++++++++-----
 core/modules/ckeditor5/js/ckeditor5.js        |  85 +++++----
 3 files changed, 177 insertions(+), 84 deletions(-)

diff --git a/core/modules/ckeditor5/ckeditor5.libraries.yml b/core/modules/ckeditor5/ckeditor5.libraries.yml
index a4f082c2c5ca..d25a96a0ca19 100644
--- a/core/modules/ckeditor5/ckeditor5.libraries.yml
+++ b/core/modules/ckeditor5/ckeditor5.libraries.yml
@@ -21,6 +21,7 @@ drupal.ckeditor5:
       css/quickedit.css: { }
   dependencies:
     - core/jquery
+    - core/once
     - core/drupal
     - core/drupal.debounce
     - core/ckeditor5.editorClassic
diff --git a/core/modules/ckeditor5/js/ckeditor5.es6.js b/core/modules/ckeditor5/js/ckeditor5.es6.js
index e5bff4ab57a5..0dbd3fcb9d07 100644
--- a/core/modules/ckeditor5/js/ckeditor5.es6.js
+++ b/core/modules/ckeditor5/js/ckeditor5.es6.js
@@ -3,7 +3,7 @@
  * CKEditor 5 implementation of {@link Drupal.editors} API.
  */
 /* global CKEditor5 */
-((Drupal, debounce, CKEditor5, $) => {
+((Drupal, debounce, CKEditor5, $, once) => {
   /**
    * The CKEDITOR instances.
    *
@@ -165,55 +165,131 @@
     });
   }
 
+  /**
+   * Process a group of CSS rules.
+   *
+   * @param {CSSGroupingRule} rulesGroup
+   *  A complete stylesheet or a group of nested rules like @media.
+   */
+  function processRules(rulesGroup) {
+    try {
+      // eslint-disable-next-line no-use-before-define
+      [...rulesGroup.cssRules].forEach(ckeditor5SelectorProcessing);
+    } catch (e) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `Stylesheet ${rulesGroup.href} not included in CKEditor reset due to the browser's CORS policy.`,
+      );
+    }
+  }
+
+  /**
+   * Processes CSS rules dynamically to account for CKEditor 5 in off canvas.
+   *
+   * This is achieved by doing the following steps:
+   * - Adding a donut scope to off canvas rules, so they don't apply within the
+   *   editor element.
+   * - Editor specific rules (i.e. those with .ck* selectors) are duplicated and
+   *   prefixed with the off canvas selector to ensure they have higher
+   *   specificity over the off canvas reset.
+   *
+   * The donut scope prevents off canvas rules from applying to the CKEditor 5
+   * editor element. Transforms a:
+   *  - #drupal-off-canvas strong
+   * rule into:
+   *  - #drupal-off-canvas strong:not([data-drupal-ck-style-fence] *)
+   *
+   * This means that the rule applies to all <strong> elements inside
+   * #drupal-off-canvas, except for <strong> elements who have a with a parent
+   * with the "data-drupal-ck-style-fence" attribute.
+   *
+   * For example:
+   * <div id="drupal-off-canvas">
+   *   <p>
+   *     <strong>Off canvas reset</strong>
+   *   </p>
+   *   <p data-drupal-ck-style-fence>
+   *     <!--
+   *       this strong elements matches the `[data-drupal-ck-style-fence] *`
+   *       selector and is excluded from the off canvas reset rule.
+   *     -->
+   *     <strong>Off canvas reset NOT applied.</strong>
+   *   </p>
+   * </div>
+   *
+   * The donut scope does not prevent CSS inheritance. There is CSS that resets
+   * following properties to prevent inheritance: background, border,
+   * box-sizing, margin, padding, position, text-decoration, transition,
+   * vertical-align and word-wrap.
+   *
+   * All .ck* CSS rules are duplicated and prefixed with the off canvas selector
+   * To ensure they have higher specificity and are not reset too aggressively.
+   *
+   * @param {CSSRule} rule
+   *  A single CSS rule to be analysed and changed if necessary.
+   */
+  function ckeditor5SelectorProcessing(rule) {
+    // Handle nested rules in @media, @support, etc.
+    if (rule.cssRules) {
+      processRules(rule);
+    }
+    if (!rule.selectorText) {
+      return;
+    }
+    const offCanvasId = '#drupal-off-canvas';
+    const CKEditorClass = '.ck';
+    const styleFence = '[data-drupal-ck-style-fence]';
+    if (
+      rule.selectorText.includes(offCanvasId) ||
+      rule.selectorText.includes(CKEditorClass)
+    ) {
+      rule.selectorText = rule.selectorText
+        .split(/,/g)
+        .map((selector) => {
+          // Only change rules that include #drupal-off-canvas in the selector.
+          if (selector.includes(offCanvasId)) {
+            return `${selector.trim()}:not(${styleFence} *)`;
+          }
+          // Duplicate CKEditor 5 styles with higher specificity for proper
+          // display in off canvas elements.
+          if (selector.includes(CKEditorClass)) {
+            // Return both rules to avoid replacing the existing rules.
+            return [
+              selector.trim(),
+              selector
+                .trim()
+                .replace(
+                  CKEditorClass,
+                  `${offCanvasId} ${styleFence} ${CKEditorClass}`,
+                ),
+            ];
+          }
+          return selector;
+        })
+        .flat()
+        .join(', ');
+    }
+  }
+
   /**
    * Adds CSS to ensure proper styling of CKEditor 5 inside off-canvas dialogs.
    *
    * @param {HTMLElement} element
    *   The element the editor is attached to.
    */
-  const offCanvasCss = (element) => {
-    element.parentNode.setAttribute('data-drupal-ck-style-fence', true);
-
+  function offCanvasCss(element) {
+    const fenceName = 'data-drupal-ck-style-fence';
+    const editor = Drupal.CKEditor5Instances.get(
+      element.getAttribute('data-ckeditor5-id'),
+    );
+    editor.ui.view.element.setAttribute(fenceName, '');
     // Only proceed if the styles haven't been added yet.
-    if (!document.querySelector('#ckeditor5-off-canvas-reset')) {
-      const prefix = `#drupal-off-canvas [data-drupal-ck-style-fence]`;
-      let existingCss = '';
-
-      // Find every existing style that doesn't come from off-canvas resets and
-      // copy them to new styles with a prefix targeting CKEditor inside an
-      // off-canvas dialog.
-      [...document.styleSheets].forEach((sheet) => {
-        if (
-          !sheet.href ||
-          (sheet.href && sheet.href.indexOf('off-canvas') === -1)
-        ) {
-          // This is wrapped in a try/catch as Chromium browsers will fail if
-          // the stylesheet was provided via a CORS request.
-          // @see https://bugs.chromium.org/p/chromium/issues/detail?id=775525
-          try {
-            const rules = sheet.cssRules;
-            [...rules].forEach((rule) => {
-              let { cssText } = rule;
-              const selector = rule.cssText.split('{')[0];
-
-              // Prefix all selectors added after a comma.
-              cssText = cssText.replace(
-                selector,
-                selector.replace(/,/g, `, ${prefix}`),
-              );
-
-              // When adding to existingCss, prefix the first selector as well.
-              existingCss += `${prefix} ${cssText}`;
-            });
-          } catch (e) {
-            // eslint-disable-next-line no-console
-            console.warn(
-              `Stylesheet ${sheet.href} not included in CKEditor reset due to the browser's CORS policy.`,
-            );
-          }
-        }
-      });
+    if (once('ckeditor5-off-canvas-reset', 'body').length) {
+      // For all rules on the page, add the donut scope for
+      // rules containing the #drupal-off-canvas selector.
+      [...document.styleSheets].forEach(processRules);
 
+      const prefix = `#drupal-off-canvas [${fenceName}]`;
       // Additional styles that need to be explicity added in addition to the
       // prefixed versions of existing css in `existingCss`.
       const addedCss = [
@@ -223,7 +299,6 @@
         `${prefix} .ck.ck-content ol li {list-style-type: decimal}`,
         `${prefix} .ck[contenteditable], ${prefix} .ck[contenteditable] * {-webkit-user-modify: read-write;-moz-user-modify: read-write;}`,
       ];
-
       // Styles to ensure block elements are displayed as such inside
       // off-canvas dialogs. These are all element types that are styled with
       // ` all: initial;` in the off-canvas reset that should default to being
@@ -268,15 +343,15 @@
         .join(', \n');
       const blockCss = `${blockSelectors} { display: block; }`;
 
-      const prefixedCss = [...addedCss, existingCss, blockCss].join('\n');
+      const prefixedCss = [...addedCss, blockCss].join('\n');
 
       // Create a new style tag with the prefixed styles added above.
-      const offCanvasCss = document.createElement('style');
-      offCanvasCss.innerHTML = prefixedCss;
-      offCanvasCss.setAttribute('id', 'ckeditor5-off-canvas-reset');
-      document.body.appendChild(offCanvasCss);
+      const offCanvasCssStyle = document.createElement('style');
+      offCanvasCssStyle.textContent = prefixedCss;
+      offCanvasCssStyle.setAttribute('id', 'ckeditor5-off-canvas-reset');
+      document.body.appendChild(offCanvasCssStyle);
     }
-  };
+  }
 
   /**
    * @namespace
@@ -593,4 +668,4 @@
       Drupal.ckeditor5.saveCallback = null;
     }
   });
-})(Drupal, Drupal.debounce, CKEditor5, jQuery);
+})(Drupal, Drupal.debounce, CKEditor5, jQuery, once);
diff --git a/core/modules/ckeditor5/js/ckeditor5.js b/core/modules/ckeditor5/js/ckeditor5.js
index 9094a9d499c1..0adfe1c98b9f 100644
--- a/core/modules/ckeditor5/js/ckeditor5.js
+++ b/core/modules/ckeditor5/js/ckeditor5.js
@@ -33,7 +33,7 @@ function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToAr
 
 function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
 
-(function (Drupal, debounce, CKEditor5, $) {
+(function (Drupal, debounce, CKEditor5, $, once) {
   Drupal.CKEditor5Instances = new Map();
   var callbacks = new Map();
   var required = new Set();
@@ -131,46 +131,63 @@ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len
     });
   }
 
-  var offCanvasCss = function offCanvasCss(element) {
-    element.parentNode.setAttribute('data-drupal-ck-style-fence', true);
-
-    if (!document.querySelector('#ckeditor5-off-canvas-reset')) {
-      var prefix = "#drupal-off-canvas [data-drupal-ck-style-fence]";
-      var existingCss = '';
-
-      _toConsumableArray(document.styleSheets).forEach(function (sheet) {
-        if (!sheet.href || sheet.href && sheet.href.indexOf('off-canvas') === -1) {
-          try {
-            var rules = sheet.cssRules;
-
-            _toConsumableArray(rules).forEach(function (rule) {
-              var cssText = rule.cssText;
-              var selector = rule.cssText.split('{')[0];
-              cssText = cssText.replace(selector, selector.replace(/,/g, ", ".concat(prefix)));
-              existingCss += "".concat(prefix, " ").concat(cssText);
-            });
-          } catch (e) {
-            console.warn("Stylesheet ".concat(sheet.href, " not included in CKEditor reset due to the browser's CORS policy."));
-          }
+  function processRules(rulesGroup) {
+    try {
+      _toConsumableArray(rulesGroup.cssRules).forEach(ckeditor5SelectorProcessing);
+    } catch (e) {
+      console.warn("Stylesheet ".concat(rulesGroup.href, " not included in CKEditor reset due to the browser's CORS policy."));
+    }
+  }
+
+  function ckeditor5SelectorProcessing(rule) {
+    if (rule.cssRules) {
+      processRules(rule);
+    }
+
+    if (!rule.selectorText) {
+      return;
+    }
+
+    var offCanvasId = '#drupal-off-canvas';
+    var CKEditorClass = '.ck';
+    var styleFence = '[data-drupal-ck-style-fence]';
+
+    if (rule.selectorText.includes(offCanvasId) || rule.selectorText.includes(CKEditorClass)) {
+      rule.selectorText = rule.selectorText.split(/,/g).map(function (selector) {
+        if (selector.includes(offCanvasId)) {
+          return "".concat(selector.trim(), ":not(").concat(styleFence, " *)");
+        }
+
+        if (selector.includes(CKEditorClass)) {
+          return [selector.trim(), selector.trim().replace(CKEditorClass, "".concat(offCanvasId, " ").concat(styleFence, " ").concat(CKEditorClass))];
         }
-      });
 
+        return selector;
+      }).flat().join(', ');
+    }
+  }
+
+  function offCanvasCss(element) {
+    var fenceName = 'data-drupal-ck-style-fence';
+    var editor = Drupal.CKEditor5Instances.get(element.getAttribute('data-ckeditor5-id'));
+    editor.ui.view.element.setAttribute(fenceName, '');
+
+    if (once('ckeditor5-off-canvas-reset', 'body').length) {
+      _toConsumableArray(document.styleSheets).forEach(processRules);
+
+      var prefix = "#drupal-off-canvas [".concat(fenceName, "]");
       var addedCss = ["".concat(prefix, " .ck.ck-content {display:block;min-height:5rem;}"), "".concat(prefix, " .ck.ck-content * {display:initial;background:initial;color:initial;padding:initial;}"), "".concat(prefix, " .ck.ck-content li {display:list-item}"), "".concat(prefix, " .ck.ck-content ol li {list-style-type: decimal}"), "".concat(prefix, " .ck[contenteditable], ").concat(prefix, " .ck[contenteditable] * {-webkit-user-modify: read-write;-moz-user-modify: read-write;}")];
       var blockSelectors = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'ol', 'ul', 'address', 'article', 'aside', 'blockquote', 'body', 'dd', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'hr', 'html', 'legend', 'main', 'menu', 'pre', 'section', 'xmp'].map(function (blockElement) {
         return "".concat(prefix, " .ck.ck-content ").concat(blockElement);
       }).join(', \n');
       var blockCss = "".concat(blockSelectors, " { display: block; }");
-      var prefixedCss = [].concat(addedCss, [existingCss, blockCss]).join('\n');
-
-      var _offCanvasCss = document.createElement('style');
-
-      _offCanvasCss.innerHTML = prefixedCss;
-
-      _offCanvasCss.setAttribute('id', 'ckeditor5-off-canvas-reset');
-
-      document.body.appendChild(_offCanvasCss);
+      var prefixedCss = [].concat(addedCss, [blockCss]).join('\n');
+      var offCanvasCssStyle = document.createElement('style');
+      offCanvasCssStyle.textContent = prefixedCss;
+      offCanvasCssStyle.setAttribute('id', 'ckeditor5-off-canvas-reset');
+      document.body.appendChild(offCanvasCssStyle);
     }
-  };
+  }
 
   Drupal.editors.ckeditor5 = {
     attach: function attach(element, format) {
@@ -367,4 +384,4 @@ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len
       Drupal.ckeditor5.saveCallback = null;
     }
   });
-})(Drupal, Drupal.debounce, CKEditor5, jQuery);
\ No newline at end of file
+})(Drupal, Drupal.debounce, CKEditor5, jQuery, once);
\ No newline at end of file
-- 
GitLab