Commit 5186bdc6 authored by johnle's avatar johnle Committed by Stephen Lucero
Browse files

Issue #3308393 by johnle, slucero, krisahil: Existing data not loaded when...

Issue #3308393 by johnle, slucero, krisahil: Existing data not loaded when multiple CKEditor instances active
parent 9f1b8890
Loading
Loading
Loading
Loading
+68 −32
Original line number Diff line number Diff line
@@ -20,6 +20,37 @@ class DrupalCKEditor extends JSONEditor.defaults.editors.string {
    this.input.setAttribute('data-schemaformat', this.input_type);
  }

  /**
   * Post-load after CKEditor has created a successful instance.
   */
  ckeditorPostLoad() {
    this.ckeditor_instance.setData(this.getValue());
    if (this.schema.readOnly || this.schema.readonly || this.schema.template) {
      this.ckeditor_instance.setReadOnly(true);
    }

    const saveEditorContent = Drupal.debounce(() => {
      this.input.value = this.ckeditor_instance.getData();
      this.refreshValue();
      // Dirty means display cache is invalidated for string editors.
      this.is_dirty = true;
      this.onChange(true);
    }, 400);

    this.ckeditor_instance.on('change', saveEditorContent);
    // In "source" mode (e.g., by clicking the "Source" button), CKEditor's
    // "change" event does not fire, so we need to listen on the "input"
    // event.
    // See https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-change
    this.ckeditor_instance.on('mode', () => {
      if (this.ckeditor_instance.mode === 'source') {
        const editable = this.ckeditor_instance.editable();
        editable.attachListener(editable, 'input', saveEditorContent);
      }
    });
    this.theme.afterInputReady(this.input);
  }

  afterInputReady() {
    if (window.CKEDITOR) {
      // Editor options.
@@ -45,7 +76,6 @@ class DrupalCKEditor extends JSONEditor.defaults.editors.string {
      if (this.schema.options.disallowedContent) {
        this.options.ckeditor_config.disallowedContent = this.schema.options.disallowedContent;
      }

      // @see Drupal.editors.ckeditor._loadExternalPlugins
      const externalPlugins = this.options.ckeditor_config.drupalExternalPlugins;
      if (externalPlugins) {
@@ -62,38 +92,32 @@ class DrupalCKEditor extends JSONEditor.defaults.editors.string {
      this.ckeditor_container = document.createElement('div');
      this.ckeditor_container.style.width = '100%';
      this.ckeditor_container.style.position = 'relative';

      this.input.style.display = 'none';

      this.input.parentNode.insertBefore(this.ckeditor_container, this.input);

      // Using the delay if delayIfDetached and delayIfDetached_callback,
      // was getting an error code: editor-delayed-creation-success, and still
      // needed to be in the timeout to work.
      setTimeout(() => {
        // The input element may be removed due to changes made via the
        // jsoneditor properties selections.
        if (this.jsoneditor.editors[this.path]) {
          this.ckeditor_instance = window.CKEDITOR.replace(this.ckeditor_container, this.options.ckeditor_config);
      this.ckeditor_instance.setData(this.getValue());
      if (this.schema.readOnly || this.schema.readonly || this.schema.template) {
        this.ckeditor_instance.setReadOnly(true);
          this.ckeditorPostLoad();
        }

      const saveEditorContent = Drupal.debounce(() => {
        this.input.value = this.ckeditor_instance.getData();
        this.refreshValue();
        // Dirty means display cache is invalidated for string editors.
        this.is_dirty = true;
        this.onChange(true);
      }, 400);

      this.ckeditor_instance.on('change', saveEditorContent);
      // In "source" mode (e.g., by clicking the "Source" button), CKEditor's
      // "change" event does not fire, so we need to listen on the "input"
      // event.
      // See https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-change
      this.ckeditor_instance.on('mode', () => {
        if (this.ckeditor_instance.mode === 'source') {
          const editable = this.ckeditor_instance.editable();
          editable.attachListener(editable, 'input', saveEditorContent);
        else {
          // Store any unloaded textarea that doesn't have a CKEditor instance
          // loaded yet.
          if (typeof window.DrupalCKEditors == 'undefined' ) {
            window.DrupalCKEditors = {};
          }
          window.DrupalCKEditors[this.path] = {
            ckeditor_container: this.ckeditor_container,
            ckeditor_config: this.options.ckeditor_config,
          }
          this.input.style.display = 'block';
        }
      });

      this.theme.afterInputReady(this.input);
      }, 1000);
    }
    else {
      super.afterInputReady();
@@ -106,6 +130,9 @@ class DrupalCKEditor extends JSONEditor.defaults.editors.string {
      window.CKEDITOR.remove(this.ckeditor_instance);
      this.ckeditor_instance = null;
    }
    if (window.DrupalCKEditors) {
      delete window.DrupalCKEditors;
    }
    super.destroy();
  }

@@ -123,11 +150,20 @@ class DrupalCKEditor extends JSONEditor.defaults.editors.string {
    if (this.always_disabled) {
      return;
    }
    // @TODO Fix this because, when the form is initially loaded, this throws a
    // JS error.

    if (this.ckeditor_instance) {
      this.ckeditor_instance.setReadOnly(false);
    }

    // Hande cases where the Ckeditor instance is removed before binding. Once
    // enabled by properties it will run only once. After being able to bind to
    // a DOM element it should be fine since it now has the instance created.
    if (window.DrupalCKEditors && window.DrupalCKEditors[this.path] && !this.ckeditor_instance) {
      const editor_config = window.DrupalCKEditors[this.path];
      this.ckeditor_instance = window.CKEDITOR.replace(editor_config.ckeditor_container, editor_config.ckeditor_config);
      this.ckeditorPostLoad();
      this.input.style.display = 'none';
    }
    super.enable();
  }

+76 −31
Original line number Diff line number Diff line
@@ -69,10 +69,49 @@ var DrupalCKEditor = /*#__PURE__*/function (_JSONEditor$defaults$) {
      this.input_type = this.schema.format;
      this.input.setAttribute('data-schemaformat', this.input_type);
    }
    /**
     * Post-load after CKEditor has created a successful instance.
     */
  }, {
    key: "ckeditorPostLoad",
    value: function ckeditorPostLoad() {
      var _this = this;
      this.ckeditor_instance.setData(this.getValue());
      if (this.schema.readOnly || this.schema.readonly || this.schema.template) {
        this.ckeditor_instance.setReadOnly(true);
      }
      var saveEditorContent = Drupal.debounce(function () {
        _this.input.value = _this.ckeditor_instance.getData();
        _this.refreshValue(); // Dirty means display cache is invalidated for string editors.
        _this.is_dirty = true;
        _this.onChange(true);
      }, 400);
      this.ckeditor_instance.on('change', saveEditorContent); // In "source" mode (e.g., by clicking the "Source" button), CKEditor's
      // "change" event does not fire, so we need to listen on the "input"
      // event.
      // See https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-change
      this.ckeditor_instance.on('mode', function () {
        if (_this.ckeditor_instance.mode === 'source') {
          var editable = _this.ckeditor_instance.editable();
          editable.attachListener(editable, 'input', saveEditorContent);
        }
      });
      this.theme.afterInputReady(this.input);
    }
  }, {
    key: "afterInputReady",
    value: function afterInputReady() {
      var _this = this;
      var _this2 = this;
      if (window.CKEDITOR) {
        // Editor options.
@@ -113,37 +152,31 @@ var DrupalCKEditor = /*#__PURE__*/function (_JSONEditor$defaults$) {
        this.ckeditor_container.style.width = '100%';
        this.ckeditor_container.style.position = 'relative';
        this.input.style.display = 'none';
        this.input.parentNode.insertBefore(this.ckeditor_container, this.input);
        this.ckeditor_instance = window.CKEDITOR.replace(this.ckeditor_container, this.options.ckeditor_config);
        this.ckeditor_instance.setData(this.getValue());
        if (this.schema.readOnly || this.schema.readonly || this.schema.template) {
          this.ckeditor_instance.setReadOnly(true);
        }
        this.input.parentNode.insertBefore(this.ckeditor_container, this.input); // Using the delay if delayIfDetached and delayIfDetached_callback,
        // was getting an error code: editor-delayed-creation-success, and still
        // needed to be in the timeout to work.
        var saveEditorContent = Drupal.debounce(function () {
          _this.input.value = _this.ckeditor_instance.getData();
          _this.refreshValue(); // Dirty means display cache is invalidated for string editors.
          _this.is_dirty = true;
          _this.onChange(true);
        }, 400);
        this.ckeditor_instance.on('change', saveEditorContent); // In "source" mode (e.g., by clicking the "Source" button), CKEditor's
        // "change" event does not fire, so we need to listen on the "input"
        // event.
        // See https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-change
        setTimeout(function () {
          // The input element may be removed due to changes made via the
          // jsoneditor properties selections.
          if (_this2.jsoneditor.editors[_this2.path]) {
            _this2.ckeditor_instance = window.CKEDITOR.replace(_this2.ckeditor_container, _this2.options.ckeditor_config);
        this.ckeditor_instance.on('mode', function () {
          if (_this.ckeditor_instance.mode === 'source') {
            var editable = _this.ckeditor_instance.editable();
            _this2.ckeditorPostLoad();
          } else {
            // Store any unloaded textarea that doesn't have a CKEditor instance
            // loaded yet.
            if (typeof window.DrupalCKEditors == 'undefined') {
              window.DrupalCKEditors = {};
            }
            editable.attachListener(editable, 'input', saveEditorContent);
            window.DrupalCKEditors[_this2.path] = {
              ckeditor_container: _this2.ckeditor_container,
              ckeditor_config: _this2.options.ckeditor_config
            };
            _this2.input.style.display = 'block';
          }
        });
        this.theme.afterInputReady(this.input);
        }, 1000);
      } else {
        _get(_getPrototypeOf(DrupalCKEditor.prototype), "afterInputReady", this).call(this);
      }
@@ -157,6 +190,10 @@ var DrupalCKEditor = /*#__PURE__*/function (_JSONEditor$defaults$) {
        this.ckeditor_instance = null;
      }
      if (window.DrupalCKEditors) {
        delete window.DrupalCKEditors;
      }
      _get(_getPrototypeOf(DrupalCKEditor.prototype), "destroy", this).call(this);
    }
  }, {
@@ -177,12 +214,20 @@ var DrupalCKEditor = /*#__PURE__*/function (_JSONEditor$defaults$) {
    value: function enable() {
      if (this.always_disabled) {
        return;
      } // @TODO Fix this because, when the form is initially loaded, this throws a
      // JS error.
      }
      if (this.ckeditor_instance) {
        this.ckeditor_instance.setReadOnly(false);
      } // Hande cases where the Ckeditor instance is removed before binding. Once
      // enabled by properties it will run only once. After being able to bind to
      // a DOM element it should be fine since it now has the instance created.
      if (window.DrupalCKEditors && window.DrupalCKEditors[this.path] && !this.ckeditor_instance) {
        var editor_config = window.DrupalCKEditors[this.path];
        this.ckeditor_instance = window.CKEDITOR.replace(editor_config.ckeditor_container, editor_config.ckeditor_config);
        this.ckeditorPostLoad();
        this.input.style.display = 'none';
      }
      _get(_getPrototypeOf(DrupalCKEditor.prototype), "enable", this).call(this);
+184 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\patternkit\FunctionalJavascript;

use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\node\Entity\Node;
use Drupal\Tests\patternkit\Traits\CKEditorIntegrationTestTrait;

/**
 * End-to-end testing for patternkit block CKEditor integration.
 *
 * @group patternkit
 */
class CKEditorIntegrationTest extends PatternkitBrowserTestBase {

  use CKEditorIntegrationTestTrait;

  /**
   * {@inheritdoc}
   */
  static protected $modules = [
    'patternkit_example',
    'ckeditor',
  ];

  /**
   * {@inheritdoc}
   */
  public function setUp(): void {
    parent::setUp();

    // Create a text format and associate CKEditor.
    FilterFormat::create([
      'format' => 'filtered_html',
      'name' => 'Filtered HTML',
      'weight' => 0,
    ])->save();
    Editor::create([
      'format' => 'filtered_html',
      'editor' => 'ckeditor',
    ])->save();

    // Enable CKEditor integration.
    $config = $this->config('patternkit.settings');
    $config->set('patternkit_json_editor_wysiwyg', 'ckeditor');
    $config->set('patternkit_json_editor_ckeditor_toolbar', 'filtered_html');
    $config->save();
  }

  /**
   * Test the end-to-end workflow of placing a new pattern on a Node layout.
   */
  public function testCkeditorVisibility(): void {
    $assert = $this->assertSession();
    $page = $this->getSession()->getPage();

    $pattern_name = '[Patternkit] Example';

    // Log in as an editor with access to update layouts.
    $this->drupalLogin($this->editorUser);

    // Override the Node layout and place a patternkit block.
    $this->drupalGet('node/1/layout');
    $page->clickLink('Add block');

    // Wait for the block list to load before selecting a pattern to insert.
    $assert->waitForLink($pattern_name)->click();

    // Wait for the JSON Editor form to load.
    $assert->waitForField('root[text]');

    // Wait for CKEditor to load.
    $this->waitForEditor('editor1');

    // Fill in JSON Editor fields.
    $page->fillField('root[text]', 'Pattern block title');
    $page->fillField('root[hidden]', 'My hidden text');

    // Fill in the CKEditor field.
    $wysiwyg_value = '<p>My <strong>rich text</strong> <em>value</em>.</p>';
    $this->setEditorValue('root.formatted_text', $wysiwyg_value);

    $config = $page->find('css', '#schema_instance_config')->getValue();
    $this->assertStringContainsString($wysiwyg_value, $config);

    $page->pressButton('Add block');
    $page->pressButton('Save layout');

    // Confirm the pattern is rendered on the Node display.
    $this->drupalGet('node/1');
    $assert->pageTextContains('Pattern block title');
    $assert->pageTextContains('My rich text value.');

    // Get block ID to return to edit form.
    $uuid = $this->getLastLayoutBlockUuid(Node::load(1));

    // Confirm all configuration content is persisted when editing the block.
    $this->drupalGet('layout_builder/update/block/overrides/node.1/0/content/' . $uuid);
    $assert->waitForField('root[text]');
    $this->waitForEditor('editor1');

    $assert->fieldValueEquals('root[text]', 'Pattern block title');
    $assert->fieldValueEquals('root[hidden]', 'My hidden text');

    // Confirm text values in CKEditor fields.
    $value = $this->getEditorValue('root.formatted_text');
    $this->assertEquals($wysiwyg_value, $value);
  }

  /**
   * Test persistence of configured CKEditor content.
   */
  public function testCkeditorPersistence(): void {
    $assert = $this->assertSession();
    $page = $this->getSession()->getPage();

    $pattern_name = '[Patternkit] Example with multiple filtered content blocks';

    // Log in as an editor with access to update layouts.
    $this->drupalLogin($this->editorUser);

    // Override the Node layout and place a patternkit block.
    $this->drupalGet('node/1/layout');
    $page->clickLink('Add block');

    // Wait for the block list to load before selecting a pattern to insert.
    $assert->waitForLink($pattern_name)->click();

    // Wait for the JSON Editor form to load.
    $assert->waitForField('root[text]');

    // Wait for CKEditor to load.
    $this->waitForEditor('editor1');

    // Confirm the CKEditor interface exists.
    $assert->elementExists('css', '#cke_editor1');
    $assert->elementExists('css', '#cke_editor2');

    // Fill in JSON Editor fields.
    $page->fillField('root[text]', 'Pattern block title');
    $page->fillField('root[hidden]', 'My hidden text');

    // Fill in the first CKEditor field.
    $restricted_value = '<p>My <strong>restricted text</strong> <em>value</em>.</p>';
    $this->setEditorValue('root.formatted_text_restricted', $restricted_value);

    // Fill in the second CKEditor field.
    $free_value = '<p>My <strong>free text</strong> <em>value</em>.</p>';
    $this->setEditorValue('root.formatted_text_free', $free_value);

    // Confirm the schema config field updated and contains CKEditor values.
    $config = $page->find('css', '#schema_instance_config')->getValue();
    $this->assertStringContainsString($restricted_value, $config);
    $this->assertStringContainsString($free_value, $config);

    $page->pressButton('Add block');
    $page->pressButton('Save layout');

    // Confirm the pattern is rendered on the Node display.
    $this->drupalGet('node/1');
    $assert->pageTextContains('Pattern block title');
    $assert->pageTextContains('My restricted text value.');
    $assert->pageTextContains('My free text value.');

    // Get component ID to return to edit form.
    $uuid = $this->getLastLayoutBlockUuid(Node::load(1));

    // Confirm all configuration content is persisted when editing the block.
    $this->drupalGet('layout_builder/update/block/overrides/node.1/0/content/' . $uuid);
    $assert->waitForField('root[text]');
    $this->waitForEditor('editor1');

    $assert->fieldValueEquals('root[text]', 'Pattern block title');
    $assert->fieldValueEquals('root[hidden]', 'My hidden text');

    // Confirm text values in CKEditor fields.
    $value_1 = $this->getEditorValue('root.formatted_text_restricted');
    $value_2 = $this->getEditorValue('root.formatted_text_free');
    $this->assertEquals($restricted_value, $value_1);
    $this->assertEquals($free_value, $value_2);
  }

}
+45 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\patternkit\Traits;

use Drupal\Tests\ckeditor\Traits\CKEditorTestTrait;

/**
 * A helper trait to interact with and test CKEditor integration.
 *
 * This trait is only intended for use in tests.
 */
trait CKEditorIntegrationTestTrait {

  use CKEditorTestTrait;

  /**
   * Get the content of a field's editor instance.
   *
   * @param string $field_name
   *   The JSON Editor field name for the field to interact with, such as
   *   'root.formatted_text'.
   *
   * @return string
   *   The returned value from the editor instance.
   */
  protected function getEditorValue(string $field_name): string {
    $js = sprintf('patternkitEditor.getEditor("%s").getValue()', $field_name);
    return $this->getSession()->evaluateScript($js);
  }

  /**
   * Get the content of a field's editor instance.
   *
   * @param string $field_name
   *   The JSON Editor field name for the field to interact with, such as
   *   'root.formatted_text'.
   * @param string $value
   *   The markup value to set in the editor.
   */
  protected function setEditorValue(string $field_name, string $value): void {
    $js = sprintf('patternkitEditor.getEditor("%s").setValue("%s")', $field_name, $value);
    $this->getSession()->evaluateScript($js);
  }

}