diff --git a/core/modules/ckeditor/ckeditor.libraries.yml b/core/modules/ckeditor/ckeditor.libraries.yml index df6881b37ae721d3f9c8fb59f5002c37036c281c..27b44afb1442309a4e6f1ea22f150951f9078673 100644 --- a/core/modules/ckeditor/ckeditor.libraries.yml +++ b/core/modules/ckeditor/ckeditor.libraries.yml @@ -2,6 +2,7 @@ drupal.ckeditor: version: VERSION js: js/ckeditor.js: {} + js/ckeditor.off-canvas-css-reset.js: {} css: state: css/ckeditor.css: {} diff --git a/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.es6.js b/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.es6.js new file mode 100644 index 0000000000000000000000000000000000000000..e1d0324a44de9cba74286be68a10d5f03c366b4b --- /dev/null +++ b/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.es6.js @@ -0,0 +1,107 @@ +/** + * @file + * Provides styles for CKEditor inside off-canvas dialogs. + */ + +(($, CKEDITOR) => { + /** + * Takes a string of CKEditor CSS and modifies it for use in off-canvas. + * + * @param {string} originalCss + * The CSS rules from CKEditor. + * @return {string} + * The rules from originalCss with extra specificity for off-canvas. + */ + const convertToOffCanvasCss = originalCss => { + const selectorPrefix = '#drupal-off-canvas '; + const skinPath = `${CKEDITOR.basePath}${CKEDITOR.skinName}/`; + const css = originalCss + .substring(originalCss.indexOf('*/') + 2) + .trim() + .replace(/}/g, `}${selectorPrefix}`) + .replace(/,/g, `,${selectorPrefix}`) + .replace(/url\(/g, skinPath); + return `${selectorPrefix}${css}`; + }; + + /** + * Inserts CSS rules into DOM. + * + * @param {string} cssToInsert + * CSS rules to be inserted + */ + const insertCss = cssToInsert => { + const offCanvasCss = document.createElement('style'); + offCanvasCss.innerHTML = cssToInsert; + offCanvasCss.setAttribute('id', 'ckeditor-off-canvas-reset'); + document.body.appendChild(offCanvasCss); + }; + + /** + * Adds CSS so CKEditor is styled properly in off-canvas. + */ + const addCkeditorOffCanvasCss = () => { + // If #ckeditor-off-canvas-reset exists, this has already run. + if (document.getElementById('ckeditor-off-canvas-reset')) { + return; + } + // CKEDITOR.skin.getPath() requires the CKEDITOR.skinName property. + // @see https://stackoverflow.com/a/17336982 + CKEDITOR.skinName = CKEDITOR.skin.name; + + // Get the paths to the css CKEditor is using. + const editorCssPath = CKEDITOR.skin.getPath('editor'); + const dialogCssPath = CKEDITOR.skin.getPath('dialog'); + + // The key for cached CSS in localStorage is based on the CSS paths. + const storedOffCanvasCss = window.localStorage.getItem( + `Drupal.off-canvas.css.${editorCssPath}${dialogCssPath}`, + ); + + // See if CSS is cached in localStorage, and use that when available. + if (storedOffCanvasCss) { + insertCss(storedOffCanvasCss); + return; + } + + // If CSS unavailable in localStorage, get the files via AJAX and parse. + $.when($.get(editorCssPath), $.get(dialogCssPath)).done( + (editorCss, dialogCss) => { + const offCanvasEditorCss = convertToOffCanvasCss(editorCss[0]); + const offCanvasDialogCss = convertToOffCanvasCss(dialogCss[0]); + const cssToInsert = `#drupal-off-canvas .cke_inner * {background: transparent;} + ${offCanvasEditorCss} + ${offCanvasDialogCss}`; + insertCss(cssToInsert); + + // The localStorage key for accessing the cached CSS is based on the + // paths of the CKEditor CSS files. This prevents localStorage from + // providing outdated CSS. If new files are used due to using a new + // skin, a new localStorage key is created. + // + // The CSS paths also include the cache-busting query string that is + // stored in state and CKEDITOR.timestamp. This query string changes on + // update and cache clear and prevents localStorage from providing + // stale CKEditor CSS. + // + // Before adding the CSS rules to localStorage, there is a check that + // confirms the cache-busting query (CKEDITOR.timestamp) is in the CSS + // paths. This prevents localStorage from caching something unbustable. + // + // @see ckeditor_library_info_alter() + if ( + CKEDITOR.timestamp && + editorCssPath.indexOf(CKEDITOR.timestamp) !== -1 && + dialogCssPath.indexOf(CKEDITOR.timestamp) !== -1 + ) { + window.localStorage.setItem( + `Drupal.off-canvas.css.${editorCssPath}${dialogCssPath}`, + cssToInsert, + ); + } + }, + ); + }; + + addCkeditorOffCanvasCss(); +})(jQuery, CKEDITOR); diff --git a/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.js b/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.js new file mode 100644 index 0000000000000000000000000000000000000000..c0738bb08f405bae98dd3f68ea3b7854f87ed0c0 --- /dev/null +++ b/core/modules/ckeditor/js/ckeditor.off-canvas-css-reset.js @@ -0,0 +1,53 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, CKEDITOR) { + var convertToOffCanvasCss = function convertToOffCanvasCss(originalCss) { + var selectorPrefix = '#drupal-off-canvas '; + var skinPath = '' + CKEDITOR.basePath + CKEDITOR.skinName + '/'; + var css = originalCss.substring(originalCss.indexOf('*/') + 2).trim().replace(/}/g, '}' + selectorPrefix).replace(/,/g, ',' + selectorPrefix).replace(/url\(/g, skinPath); + return '' + selectorPrefix + css; + }; + + var insertCss = function insertCss(cssToInsert) { + var offCanvasCss = document.createElement('style'); + offCanvasCss.innerHTML = cssToInsert; + offCanvasCss.setAttribute('id', 'ckeditor-off-canvas-reset'); + document.body.appendChild(offCanvasCss); + }; + + var addCkeditorOffCanvasCss = function addCkeditorOffCanvasCss() { + if (document.getElementById('ckeditor-off-canvas-reset')) { + return; + } + + CKEDITOR.skinName = CKEDITOR.skin.name; + + var editorCssPath = CKEDITOR.skin.getPath('editor'); + var dialogCssPath = CKEDITOR.skin.getPath('dialog'); + + var storedOffCanvasCss = window.localStorage.getItem('Drupal.off-canvas.css.' + editorCssPath + dialogCssPath); + + if (storedOffCanvasCss) { + insertCss(storedOffCanvasCss); + return; + } + + $.when($.get(editorCssPath), $.get(dialogCssPath)).done(function (editorCss, dialogCss) { + var offCanvasEditorCss = convertToOffCanvasCss(editorCss[0]); + var offCanvasDialogCss = convertToOffCanvasCss(dialogCss[0]); + var cssToInsert = '#drupal-off-canvas .cke_inner * {background: transparent;}\n ' + offCanvasEditorCss + '\n ' + offCanvasDialogCss; + insertCss(cssToInsert); + + if (CKEDITOR.timestamp && editorCssPath.indexOf(CKEDITOR.timestamp) !== -1 && dialogCssPath.indexOf(CKEDITOR.timestamp) !== -1) { + window.localStorage.setItem('Drupal.off-canvas.css.' + editorCssPath + dialogCssPath, cssToInsert); + } + }); + }; + + addCkeditorOffCanvasCss(); +})(jQuery, CKEDITOR); \ No newline at end of file diff --git a/core/modules/ckeditor/tests/modules/ckeditor_test.routing.yml b/core/modules/ckeditor/tests/modules/ckeditor_test.routing.yml index 833edc6a3bca35d3a1398d08ab9bffcd52f74ad3..01aeca0e2dfb9da71ad845701094109b105d0b0a 100644 --- a/core/modules/ckeditor/tests/modules/ckeditor_test.routing.yml +++ b/core/modules/ckeditor/tests/modules/ckeditor_test.routing.yml @@ -5,3 +5,10 @@ ckeditor_test.ajax_css: _form: '\Drupal\ckeditor_test\Form\AjaxCssForm' requirements: _access: 'TRUE' + +ckeditor_test.off_canvas: + path: '/ckeditor_test/off_canvas' + defaults: + _controller: '\Drupal\ckeditor_test\CkeditorOffCanvasTestController::testOffCanvas' + requirements: + _access: 'TRUE' diff --git a/core/modules/ckeditor/tests/modules/src/CkeditorOffCanvasTestController.php b/core/modules/ckeditor/tests/modules/src/CkeditorOffCanvasTestController.php new file mode 100644 index 0000000000000000000000000000000000000000..7f02ced45a2cddc00956bffa5f0625c379014f08 --- /dev/null +++ b/core/modules/ckeditor/tests/modules/src/CkeditorOffCanvasTestController.php @@ -0,0 +1,33 @@ +<?php + +namespace Drupal\ckeditor_test; + +use Drupal\Core\Url; + +/** + * Provides controller for testing CKEditor in off-canvas dialogs. + */ +class CkeditorOffCanvasTestController { + + /** + * Returns a link that can open a node add form in an off-canvas dialog. + * + * @return array + * A render array. + */ + public function testOffCanvas() { + $build['link'] = [ + '#type' => 'link', + '#title' => 'Add Node', + '#url' => Url::fromRoute('node.add', ['node_type' => 'page']), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', + ], + ]; + $build['#attached']['library'][] = 'core/drupal.dialog.off_canvas'; + return $build; + } + +} diff --git a/core/modules/ckeditor/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php b/core/modules/ckeditor/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php index 9e7b8df53a6ee2d22d166da6ff8175f62a7e922f..1a1a5cebaa19135318b8a8278b34cf1049d6d810 100644 --- a/core/modules/ckeditor/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php +++ b/core/modules/ckeditor/tests/src/FunctionalJavascript/CKEditorIntegrationTest.php @@ -34,7 +34,7 @@ class CKEditorIntegrationTest extends WebDriverTestBase { /** * {@inheritdoc} */ - public static $modules = ['node', 'ckeditor', 'filter']; + public static $modules = ['node', 'ckeditor', 'filter', 'ckeditor_test']; /** * {@inheritdoc} @@ -177,4 +177,30 @@ public function testDrupalImageCaptionDialog() { $web_assert->elementExists('css', '.ui-dialog input[name="attributes[hasCaption]"]'); } + /** + * Tests if CKEditor is properly styled inside an off-canvas dialog. + */ + public function testOffCanvasStyles() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalGet('/ckeditor_test/off_canvas'); + + // The "Add Node" link triggers an off-canvas dialog with an add node form + // that includes CKEditor. + $page->clickLink('Add Node'); + $assert_session->waitForElementVisible('css', '#drupal-off-canvas'); + $assert_session->assertWaitOnAjaxRequest(); + + // Check the background color of two CKEditor elements to confirm they are + // not overriden by the off-canvas css reset. + $assert_session->elementExists('css', '.cke_top'); + $ckeditor_top_bg_color = $this->getSession()->evaluateScript('window.getComputedStyle(document.getElementsByClassName(\'cke_top\')[0]).backgroundColor'); + $this->assertEqual($ckeditor_top_bg_color, 'rgb(248, 248, 248)'); + + $assert_session->elementExists('css', '.cke_button__source'); + $ckeditor_source_button_bg_color = $this->getSession()->evaluateScript('window.getComputedStyle(document.getElementsByClassName(\'cke_button__source\')[0]).backgroundColor'); + $this->assertEqual($ckeditor_source_button_bg_color, 'rgba(0, 0, 0, 0)'); + } + }