From e56a9d3769c7903f1ea744cc8f8399eacfd5097c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Fri, 18 Oct 2024 18:54:29 +0200 Subject: [PATCH] HtmlEditor: Implement converter option --- .../ui/html_editor/m_html_editor.ts | 27 ++- .../htmlEditorParts/api.tests.js | 214 ++++++++++++++++++ .../htmlEditorParts/valueRendering.tests.js | 16 ++ .../chatParts/chat.tests.js | 2 - 4 files changed, 252 insertions(+), 7 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/html_editor/m_html_editor.ts b/packages/devextreme/js/__internal/ui/html_editor/m_html_editor.ts index 06b24c7250f5..9fe17a38b91c 100644 --- a/packages/devextreme/js/__internal/ui/html_editor/m_html_editor.ts +++ b/packages/devextreme/js/__internal/ui/html_editor/m_html_editor.ts @@ -57,13 +57,11 @@ const HtmlEditor = Editor.inherit({ customizeModules: null, tableContextMenu: null, allowSoftLineBreak: false, - formDialogOptions: null, - imageUpload: null, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing stylingMode: config().editorStylingMode || 'outlined', + converter: null, }); }, @@ -264,6 +262,12 @@ const HtmlEditor = Editor.inherit({ this._deltaConverter = new DeltaConverter(); } } + + const { converter } = this.option(); + + if (converter) { + this._htmlConverter = converter; + } }, _renderContentImpl() { @@ -428,7 +432,12 @@ const HtmlEditor = Editor.inherit({ _textChangeHandler() { const { value: currentValue } = this.option(); - const convertedValue = this._deltaConverter.toHtml(); + + const htmlMarkup = this._deltaConverter.toHtml(); + + const convertedValue = isFunction(this._htmlConverter?.fromHtml) + ? String(this._htmlConverter.fromHtml(htmlMarkup)) + : htmlMarkup; if ( currentValue !== convertedValue @@ -505,13 +514,21 @@ const HtmlEditor = Editor.inherit({ _optionChanged(args) { switch (args.name) { + case 'converter': { + this._htmlConverter = args.value; + break; + } case 'value': { if (this._quillInstance) { if (this._isEditorUpdating) { this._isEditorUpdating = false; } else { + const updatedValue = isFunction(this._htmlConverter?.toHtml) + ? String(this._htmlConverter.toHtml(args.value)) + : args.value; + this._suppressValueChangeAction(); - this._updateHtmlContent(args.value); + this._updateHtmlContent(updatedValue); this._resumeValueChangeAction(); } } else { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/api.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/api.tests.js index e3aad7ef7ae3..b08ed2f90470 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/api.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/api.tests.js @@ -3,12 +3,14 @@ import $ from 'jquery'; import 'ui/html_editor'; import { prepareEmbedValue, prepareTableValue } from './utils.js'; import { isObject } from 'core/utils/type'; +import keyboardMock from '../../../helpers/keyboardMock.js'; const { test, module: testModule } = QUnit; const TOOLBAR_FORMAT_WIDGET_CLASS = 'dx-htmleditor-toolbar-format'; const DISABLED_STATE_CLASS = 'dx-state-disabled'; const BUTTON_CLASS = 'dx-button'; +const HTML_EDITOR_CONTENT_CLASS = 'dx-htmleditor-content'; const moduleConfig = { beforeEach: function() { @@ -494,6 +496,218 @@ testModule('API', moduleConfig, () => { this.clock.tick(10); assert.ok(this.options.onContentReady.calledOnce, 'onContentReady has been called once'); }); + + testModule('converter option', () => { + test('toHtml and fromHtml should be called once after value option changed', function(assert) { + const converter = { + toHtml: sinon.stub(), + fromHtml: sinon.stub(), + }; + this.options = { converter }; + + this.createEditor(); + + this.instance.option('value', 'new value'); + + assert.strictEqual(this.options.converter.toHtml.callCount, 1); + assert.strictEqual(this.options.converter.fromHtml.callCount, 1); + }); + + test('toHtml and fromHtml must be called the correct number of times after the character has been entered', function(assert) { + assert.expect(2); + + const done = assert.async(); + + const converter = { + toHtml: sinon.stub(), + fromHtml: sinon.stub(), + }; + + this.options = { + converter, + onValueChanged: () => { + assert.strictEqual(converter.toHtml.callCount, 0); + assert.strictEqual(converter.fromHtml.callCount, 1); + + done(); + }, + }; + + this.createEditor(); + + this.instance.focus(); + + const input = this.instance.$element().find(`.${HTML_EDITOR_CONTENT_CLASS}`).get(0); + + keyboardMock(input).type('t').change(); + input.textContent = 't'; + }); + + [ + '', + 'string', + true, + false, + null, + undefined, + NaN, + 4, + Infinity, + -Infinity, + {}, + ].forEach(value => { + test(`There is no error here if the toHtml return value is ${value}`, function(assert) { + this.options = { + converter: { + toHtml: () => value, + fromHtml: (e) => e, + }, + }; + + this.createEditor(); + + try { + this.instance.option('value', ''); + } catch(e) { + assert.ok(false, `error: ${e.message}`); + } finally { + assert.ok(true, 'there is no error'); + } + }); + + test(`There is no error here if the fromHtml return value is ${value}`, function(assert) { + this.options = { + converter: { + toHtml: (e) => e, + fromHtml: () => value, + }, + }; + + this.createEditor(); + + try { + this.instance.option('value', ''); + } catch(e) { + assert.ok(false, `error: ${e.message}`); + } finally { + assert.ok(true, 'there is no error'); + } + }); + + test(`There is no error here if toHtml is ${value}`, function(assert) { + this.options = { + converter: { + toHtml: value, + fromHtml: (val) => val, + } + }; + + this.createEditor(); + + try { + this.instance.option('value', ''); + } catch(e) { + assert.ok(false, `error: ${e.message}`); + } finally { + assert.ok(true, 'there is no error'); + } + }); + + test(`There is no error here if fromHtml is ${value}`, function(assert) { + this.options = { + converter: { + toHtml: (val) => val, + fromHtml: value, + }, + }; + + this.createEditor(); + + try { + this.instance.option('value', ''); + } catch(e) { + assert.ok(false, `error: ${e.message}`); + } finally { + assert.ok(true, 'there is no error'); + } + }); + }); + + test('converter option runtime change should update html converter', function(assert) { + const firstConverter = { + toHtml: sinon.stub(), + fromHtml: sinon.stub(), + }; + + const secondConverter = { + toHtml: sinon.stub(), + fromHtml: sinon.stub(), + }; + + const instance = $('#htmlEditor').dxHtmlEditor({ + converter: firstConverter, + }).dxHtmlEditor('instance'); + + instance.option('converter', secondConverter); + instance.option('value', 'new value'); + + assert.strictEqual(firstConverter.toHtml.callCount, 0); + assert.strictEqual(firstConverter.fromHtml.callCount, 0); + assert.strictEqual(secondConverter.toHtml.callCount, 1); + assert.strictEqual(secondConverter.fromHtml.callCount, 1); + }); + + test('The converter methods get the correct parameters', function(assert) { + const converter = { + toHtml: (value) => { + assert.strictEqual(value, 'new value'); + + return value; + }, + fromHtml: (value) => { + assert.strictEqual(value, '

new value

'); + + return value; + }, + }; + + this.options = { converter }; + + this.createEditor(); + + this.instance.option('value', 'new value'); + }); + + test('toHtml changes value correctly', function(assert) { + const converter = { + toHtml: () => { + return '

NEW VALUE

'; + }, + }; + this.options = { converter }; + + this.createEditor(); + + this.instance.option('value', 'new value'); + + assert.strictEqual(this.instance.option('value'), '

NEW VALUE

'); + }); + + test('fromHtml changes value correctly', function(assert) { + const converter = { + fromHtml: () => { + return '**NEW VALUE**'; + }, + }; + this.options = { converter }; + + this.createEditor(); + + this.instance.option('value', 'new value'); + + assert.strictEqual(this.instance.option('value'), '**NEW VALUE**'); + }); + }); }); testModule('Private API', moduleConfig, () => { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/valueRendering.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/valueRendering.tests.js index e48db94e758f..45afa1899aa2 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/valueRendering.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/valueRendering.tests.js @@ -649,4 +649,20 @@ export default function() { }); }); }); + + testModule('converter option', () => { + test('value from toHtml must match the markup value', function(assert) { + const instance = $('#htmlEditor').dxHtmlEditor({ + converter: { + toHtml: () => '

Hi!

Test

', + }, + }).dxHtmlEditor('instance'); + + instance.option('value', 'new value'); + + const markup = instance.$element().find(`.${CONTENT_CLASS}`).html(); + + assert.strictEqual(markup, '

Hi!

Test

'); + }); + }); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js index 555881911e56..1d75213e2966 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js @@ -706,5 +706,3 @@ QUnit.module('Chat', moduleConfig, () => { }); }); }); - -