From 3c7808e44637cf63977adea04f5eebbfb189db83 Mon Sep 17 00:00:00 2001 From: Davide Diaconu Date: Wed, 7 Feb 2024 15:35:26 +0200 Subject: [PATCH 1/2] fix: cursor jumping at the beginning --- .../src/main/ts/editor/editor.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tinymce-angular-component/src/main/ts/editor/editor.component.ts b/tinymce-angular-component/src/main/ts/editor/editor.component.ts index 7fcec109..f6fd4b89 100644 --- a/tinymce-angular-component/src/main/ts/editor/editor.component.ts +++ b/tinymce-angular-component/src/main/ts/editor/editor.component.ts @@ -88,7 +88,9 @@ export class EditorComponent extends Events implements AfterViewInit, ControlVal public writeValue(value: string | null): void { if (this._editor && this._editor.initialized) { + const cursor = this._editor.selection.getBookmark(3); this._editor.setContent(isNullOrUndefined(value) ? '' : value); + this._editor.selection.moveToBookmark(cursor); } else { this.initialValue = value === null ? undefined : value; } From da83820a8c0d4a07c95b3044ab44d37342f0d59d Mon Sep 17 00:00:00 2001 From: Davide Diaconu Date: Thu, 28 Mar 2024 15:35:37 +0200 Subject: [PATCH 2/2] test: fixed breaking test and add test for FormControl cursor issue --- .../src/main/ts/editor/editor.component.ts | 6 +- .../src/test/ts/browser/FormControlTest.ts | 116 ++++++++++++------ .../src/test/ts/browser/LoadTinyTest.ts | 4 +- 3 files changed, 83 insertions(+), 43 deletions(-) diff --git a/tinymce-angular-component/src/main/ts/editor/editor.component.ts b/tinymce-angular-component/src/main/ts/editor/editor.component.ts index f6fd4b89..05d46c27 100644 --- a/tinymce-angular-component/src/main/ts/editor/editor.component.ts +++ b/tinymce-angular-component/src/main/ts/editor/editor.component.ts @@ -90,7 +90,11 @@ export class EditorComponent extends Events implements AfterViewInit, ControlVal if (this._editor && this._editor.initialized) { const cursor = this._editor.selection.getBookmark(3); this._editor.setContent(isNullOrUndefined(value) ? '' : value); - this._editor.selection.moveToBookmark(cursor); + try { + this._editor.selection.moveToBookmark(cursor); + } catch (e) { + /* ignore */ + } } else { this.initialValue = value === null ? undefined : value; } diff --git a/tinymce-angular-component/src/test/ts/browser/FormControlTest.ts b/tinymce-angular-component/src/test/ts/browser/FormControlTest.ts index 1599cc92..cd789528 100644 --- a/tinymce-angular-component/src/test/ts/browser/FormControlTest.ts +++ b/tinymce-angular-component/src/test/ts/browser/FormControlTest.ts @@ -8,11 +8,12 @@ import { Assertions, Chain, Log, Pipeline } from '@ephox/agar'; import { UnitTest } from '@ephox/bedrock-client'; import { VersionLoader } from '@tinymce/miniature'; +import { Editor } from 'tinymce'; import { EditorComponent, EditorModule } from '../../../main/ts/public_api'; UnitTest.asynctest('FormControlTest', (success, failure) => { @Component({ - template: `` + template: ``, }) class EditorWithFormControl { public control = new FormControl(); @@ -21,7 +22,7 @@ UnitTest.asynctest('FormControlTest', (success, failure) => { interface TestContext { testComponent: EditorWithFormControl; fixture: ComponentFixture; - editor: any; + editor: Editor; } const cSetupEditorWithFormControl = Chain.async((_, next) => { @@ -53,42 +54,77 @@ UnitTest.asynctest('FormControlTest', (success, failure) => { TestBed.resetTestingModule(); }); - const sTestVersion = (version: '4' | '5' | '6') => VersionLoader.sWithVersion( - version, - Log.chainsAsStep('', 'FormControl interaction ', [ - cSetupEditorWithFormControl, - Chain.op((context: TestContext) => { - Assertions.assertEq( - 'Expect editor to have no initial value', - '', - context.editor.getContent() - ); - - context.testComponent.control.setValue('

Some Value

'); - context.fixture.detectChanges(); - - Assertions.assertEq( - 'Expect editor to have a value', - '

Some Value

', - context.editor.getContent() - ); - - context.testComponent.control.reset(); - context.fixture.detectChanges(); - - Assertions.assertEq( - 'Expect editor to be empty after reset', - '', - context.editor.getContent() - ); - }), - cTeardown - ]) - ); + const setFormValueAndReset = Chain.op((context: TestContext) => { + Assertions.assertEq('Expect editor to have no initial value', '', context.editor.getContent()); + + context.testComponent.control.setValue('

Some Value

'); + context.fixture.detectChanges(); + + Assertions.assertEq( + 'Expect editor to have a value', + '

Some Value

', + context.editor.getContent() + ); + + context.testComponent.control.reset(); + context.fixture.detectChanges(); + + Assertions.assertEq('Expect editor to be empty after reset', '', context.editor.getContent()); + }); + + /** Sets content and set cursor at the end, just like when a real user types */ + const doFakeType = (str: string) => + Chain.op((context: TestContext) => { + context.testComponent.control.patchValue(`${str}`); + context.fixture.detectChanges(); + + const rng = context.editor.dom.createRng(); + const firstChild = context.editor.getBody().firstChild?.firstChild as Text; + rng.setStart(firstChild, str.length); + rng.setEnd(firstChild, str.length); - Pipeline.async({}, [ - sTestVersion('4'), - sTestVersion('5'), - sTestVersion('6') - ], success, failure); -}); \ No newline at end of file + context.editor.selection.setRng(rng); + context.fixture.detectChanges(); + }); + + const checkCursor = Chain.op((context: TestContext) => { + const cursorStartPosition = context.editor.selection.getRng().startOffset; + const cursorEndPosition = context.editor.selection.getRng().endOffset; + const content = context.editor.getContent(); + const strippedContent = content.replace(/<\/?[^>]+(>|$)/g, ''); + Assertions.assertEq( + 'Expect cursor start position to be at the end of the content', + cursorStartPosition, + strippedContent.length + ); + + Assertions.assertEq( + 'Expect cursor end position to be at the end of the content', + cursorEndPosition, + strippedContent.length + ); + }); + + const sTestVersion = (version: '4' | '5' | '6') => + VersionLoader.sWithVersion( + version, + Log.chainsAsStep('', 'FormControl interaction ', [ + cSetupEditorWithFormControl, + setFormValueAndReset, + doFakeType('test'), + checkCursor, + cTeardown, + ]) + ); + + Pipeline.async( + {}, + [ + sTestVersion('4'), + sTestVersion('5'), + sTestVersion('6'), + ], + success, + failure + ); +}); diff --git a/tinymce-angular-component/src/test/ts/browser/LoadTinyTest.ts b/tinymce-angular-component/src/test/ts/browser/LoadTinyTest.ts index 7487a51c..67a1e9a7 100644 --- a/tinymce-angular-component/src/test/ts/browser/LoadTinyTest.ts +++ b/tinymce-angular-component/src/test/ts/browser/LoadTinyTest.ts @@ -60,7 +60,7 @@ UnitTest.asynctest('LoadTinyTest', (success, failure) => { TestBed.resetTestingModule(); }); - const cAssertTinymceVersion = (version: '4' | '5' | '6') => Chain.op(() => { + const cAssertTinymceVersion = (version: '4' | '5' | '6' | '7') => Chain.op(() => { Assertions.assertEq(`Loaded version of TinyMCE should be ${version}`, version, Global.tinymce.majorVersion); }); @@ -85,7 +85,7 @@ UnitTest.asynctest('LoadTinyTest', (success, failure) => { ]), Log.chainsAsStep('Should be able to load TinyMCE from Cloud', '', [ cSetupEditor([ 'apiKey="a-fake-api-key"', 'cloudChannel="6-dev"' ], []), - cAssertTinymceVersion('6'), + cAssertTinymceVersion('7'), Chain.op(() => { Assertions.assertEq( 'TinyMCE should have been loaded from Cloud',