Loading js/patternkit.jsoneditor.ckeditor.es6.js +68 −32 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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) { Loading @@ -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(); Loading @@ -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(); } Loading @@ -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(); } Loading js/patternkit.jsoneditor.js +76 −31 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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); } Loading @@ -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); } }, { Loading @@ -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); Loading tests/src/FunctionalJavascript/CKEditorIntegrationTest.php 0 → 100644 +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); } } tests/src/Traits/CKEditorIntegrationTestTrait.php 0 → 100644 +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); } } Loading
js/patternkit.jsoneditor.ckeditor.es6.js +68 −32 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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) { Loading @@ -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(); Loading @@ -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(); } Loading @@ -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(); } Loading
js/patternkit.jsoneditor.js +76 −31 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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); } Loading @@ -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); } }, { Loading @@ -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); Loading
tests/src/FunctionalJavascript/CKEditorIntegrationTest.php 0 → 100644 +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); } }
tests/src/Traits/CKEditorIntegrationTestTrait.php 0 → 100644 +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); } }