Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@ Thumbs.db

# release
/release/

# Vitest

.vitest-attachments/
__screenshots__/
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@angular/platform-browser": "^19.2.19",
"@angular/platform-browser-dynamic": "^19.2.19",
"@angular/router": "^19.2.19",
"@ckeditor/ckeditor5-integrations-common": "^2.2.5",
"@ckeditor/ckeditor5-integrations-common": "^2.3.0",
"core-js": "^3.21.1",
"rxjs": "^6.5.5",
"tslib": "^2.0.3",
Expand Down
2,057 changes: 1,242 additions & 815 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/app/simple-cdn-usage/simple-cdn-usage.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ <h3>Classic build</h3>
(ready)="onReady()"
(change)="onChange()"
(focus)="onFocus()"
(error)="onError()"
(error)="onError($event)"
(blur)="onBlur()">
</ckeditor>

Expand Down
3 changes: 2 additions & 1 deletion src/app/simple-cdn-usage/simple-cdn-usage.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ You learn to appreciate each and every single one of the differences while you b
this.componentEvents.push( 'Blurred the editing view.' );
}

public onError(): void {
public onError( err: any ): void {
this.componentEvents.push( 'The editor crashed.' );
console.error( err );
}
}
4 changes: 2 additions & 2 deletions src/ckeditor/ckeditor.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,13 @@ describe( 'CKEditorComponent', () => {
expect( component.editorInstance!.data.get() ).toEqual( '<p>foo</p>' );
} );

it( 'should not be provided using both `config.initialData` or `data` properties', async () => {
it( 'should be provided using both `config.initialData` or `data` properties', async () => {
component.config = { initialData: 'foo' };
component.data = 'bar';

await expect( () => {
fixture.detectChanges();
} ).toThrowError( 'Editor data should be provided either using `config.initialData` or `data` properties.' );
} ).not.to.throw();
} );

it( 'should be writeable by ControlValueAccessor', async () => {
Expand Down
120 changes: 70 additions & 50 deletions src/ckeditor/ckeditor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '@angular/core';

import { first } from 'rxjs/operators';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms';

import type {
ContextWatchdog,
Expand All @@ -29,16 +29,21 @@ import type {
EditorConfig,
GetEventInfo,
ModelDocumentChangeEvent,
EditorWatchdogCreatorFunction,
ViewDocumentBlurEvent,
ViewDocumentFocusEvent
ViewDocumentFocusEvent,
ContextWatchdogItemConfiguration
} from 'ckeditor5';
import type { ControlValueAccessor } from '@angular/forms';

import { uid } from '@ckeditor/ckeditor5-integrations-common';
import {
assignElementToEditorConfig,
assignInitialDataToEditorConfig,
getInstalledCKBaseFeatures,
type EditorRelaxedConstructor
} from '@ckeditor/ckeditor5-integrations-common';

import { compareInstalledCKBaseVersion, uid } from '@ckeditor/ckeditor5-integrations-common';
import { appendAllIntegrationPluginsToConfig } from './plugins/append-all-integration-plugins-to-config';
import { DisabledEditorWatchdog } from './disabled-editor-watchdog';
import { assignInitialDataToEditorConfig } from './utils/assignInitialDataToEditorConfig';
import { DisabledEditorWatchdog, type EditorRelaxedCreatorFunction } from './disabled-editor-watchdog';

const ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID = 'Lock from Angular integration (@ckeditor/ckeditor5-angular)';

Expand Down Expand Up @@ -80,8 +85,7 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
* The constructor of the editor to be used for the instance of the component.
* It can be e.g. the `ClassicEditorBuild`, `InlineEditorBuild` or some custom editor.
*/
@Input() public editor?: {
create( sourceElementOrData: HTMLElement | string, config?: EditorConfig ): Promise<TEditor>;
@Input() public editor?: EditorRelaxedConstructor<TEditor> & {
EditorWatchdog: typeof EditorWatchdog;
};

Expand Down Expand Up @@ -234,11 +238,6 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
*/
private cvaOnTouched?: () => void;

/**
* Reference to the source element used by the editor.
*/
private editorElement?: HTMLElement;

/**
* A lock flag preventing from calling the `cvaOnChange()` during setting editor data.
*/
Expand All @@ -262,24 +261,7 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
this.ngZone = ngZone;
this.elementRef = elementRef;

this.checkVersion();
}

private checkVersion() {
// To avoid issues with the community typings and CKEditor 5, let's treat window as any. See #342.
const { CKEDITOR_VERSION } = ( window as any );

if ( !CKEDITOR_VERSION ) {
return console.warn( 'Cannot find the "CKEDITOR_VERSION" in the "window" scope.' );
}

const [ major ] = CKEDITOR_VERSION.split( '.' ).map( Number );

if ( major >= 42 || CKEDITOR_VERSION.startsWith( '0.0.0' ) ) {
return;
}

console.warn( 'The <CKEditor> component requires using CKEditor 5 in version 42+ or nightly build.' );
assertMinimumSupportedVersion();
}

// Implementing the OnChanges interface. Whenever the `data` property is changed, update the editor content.
Expand Down Expand Up @@ -381,11 +363,20 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
* because of the issue in the collaboration mode (#6).
*/
private attachToWatchdog() {
const creator: EditorWatchdogCreatorFunction<TEditor> = ( ( elementOrData, config ) => {
const Editor = this.editor!;

const supports = getInstalledCKBaseFeatures();
const element = document.createElement( this.tagName );

const creator = ( config: EditorConfig ) => {
return this.ngZone.runOutsideAngular( async () => {
this.elementRef.nativeElement.appendChild( elementOrData as HTMLElement );
this.elementRef.nativeElement.appendChild( element );

const editor = await this.editor!.create( elementOrData as HTMLElement, config );
const editor = await (
supports.elementConfigAttachment ?
Editor.create( assignElementToEditorConfig( Editor, element, config ) ) :
Editor.create( element, config )
);

if ( this.initiallyDisabled ) {
editor.enableReadOnlyMode( ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID );
Expand All @@ -399,12 +390,12 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After

return editor;
} );
} );
};

const destructor = async ( editor: Editor ) => {
await editor.destroy();

this.elementRef.nativeElement.removeChild( this.editorElement! );
this.elementRef.nativeElement.removeChild( element );
};

const emitError = ( e?: unknown ) => {
Expand All @@ -419,22 +410,32 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
console.error( e );
}
};
const element = document.createElement( this.tagName );
const config = this.getConfig();

this.editorElement = element;

// Based on the presence of the watchdog decide how to initialize the editor.
if ( this.watchdog && !this.disableWatchdog ) {
// When the context watchdog is passed add the new item to it based on the passed configuration.
this.watchdog.add( {
let watchdogConfig: Record<string, any> = {
id: this.id,
type: 'editor',
creator,
destructor,
sourceElementOrData: element,
config
} ).catch( e => {
config: this.getConfig()
};

/* istanbul ignore next -- @preserve */
if ( supports.elementConfigAttachment ) {
watchdogConfig = {
...watchdogConfig,
creator
};
} else {
watchdogConfig = {
...watchdogConfig,
creator: ( _: HTMLElement, config: EditorConfig ) => creator( config ),
sourceElementOrData: element
};
}

// When the context watchdog is passed add the new item to it based on the passed configuration.
this.watchdog.add( watchdogConfig as ContextWatchdogItemConfiguration ).catch( e => {
emitError( e );
} );

Expand All @@ -448,6 +449,7 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
} else {
// In the other case create the watchdog by hand to keep the editor running.
const WatchdogClass = this.disableWatchdog ? DisabledEditorWatchdog : this.editor!.EditorWatchdog;
const config = this.getConfig();

const editorWatchdog = new WatchdogClass(
this.editor!,
Expand All @@ -462,10 +464,13 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
}

this.editorWatchdog = editorWatchdog;

// Note: must be called outside of the Angular zone too because `create` is calling
// `_startErrorHandling` within a microtask, which sets up `error` listener on the window.
this.ngZone.runOutsideAngular( () => {
// Note: must be called outside of the Angular zone too because `create` is calling
// `_startErrorHandling` within a microtask, which sets up `error` listener on the window.
editorWatchdog.create( element, config ).catch( e => {
const create = editorWatchdog.create.bind( editorWatchdog ) as EditorRelaxedCreatorFunction;

create( config ).catch( e => {
emitError( e );
} );
} );
Expand Down Expand Up @@ -525,6 +530,21 @@ function hasObservers<T>( emitter: EventEmitter<T> ): boolean {
return ( emitter as any ).observed || emitter.observers.length > 0;
}

/**
* Checks if currently installed version of the editor is supported by the integration.
*/
function assertMinimumSupportedVersion(): void {
switch ( compareInstalledCKBaseVersion( '42.0.0' ) ) {
case null:
console.warn( 'Cannot find the "CKEDITOR_VERSION" in the "window" scope.' );
break;

case -1:
console.warn( 'The <CKEditor> component requires using CKEditor 5 in version 42+ or nightly build.' );
break;
}
}

/**
* Temporarily use the `_watchdogs` internal map as the `getItem()` method throws
* an error when the item is not registered yet.
Expand Down
2 changes: 1 addition & 1 deletion src/ckeditor/disabled-editor-watchdog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe( 'DisabledEditorWatchdog', () => {

const element = document.createElement( 'div' );

await watchdog.create( element );
await watchdog.create( element, {} );

expect( creator ).toHaveBeenCalledWith( element, {} );
expect( watchdog.editor ).toBe( editor );
Expand Down
26 changes: 13 additions & 13 deletions src/ckeditor/disabled-editor-watchdog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

import type {
Editor,
EditorConfig,
EditorWatchdogCreatorFunction,
WatchdogConfig
} from 'ckeditor5';
import type { EditorRelaxedConstructor } from '@ckeditor/ckeditor5-integrations-common';
import type { Editor, EditorConfig, WatchdogConfig } from 'ckeditor5';

/**
* A dummy watchdog class is used when the watchdog is disabled.
Expand All @@ -24,7 +20,7 @@ export class DisabledEditorWatchdog<TEditor extends Editor = Editor> {
/**
* The creator function.
*/
private _creator?: EditorWatchdogCreatorFunction<TEditor>;
private _creator?: EditorRelaxedCreatorFunction<TEditor>;

/**
* The destructor function.
Expand All @@ -34,17 +30,15 @@ export class DisabledEditorWatchdog<TEditor extends Editor = Editor> {
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_editorConstructor: {
create( sourceElementOrData: HTMLElement | string, config?: EditorConfig ): Promise<TEditor>;
},
_editorConstructor: EditorRelaxedConstructor<TEditor>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_config?: WatchdogConfig
) {}

/**
* Sets the creator function.
*/
public setCreator( creator: EditorWatchdogCreatorFunction<TEditor> ): void {
public setCreator( creator: EditorRelaxedCreatorFunction<TEditor> ): void {
this._creator = creator;
}

Expand All @@ -66,8 +60,12 @@ export class DisabledEditorWatchdog<TEditor extends Editor = Editor> {
/**
* Creates the editor instance.
*/
public async create( elementOrData: HTMLElement | string, config?: EditorConfig ): Promise<void> {
this.editor = await this._creator!( elementOrData, config || /* istanbul ignore next */ ( {} as any ) );
public async create( sourceElementOrData: HTMLElement | string, config: EditorConfig ): Promise<unknown>;

public async create( config: EditorConfig ): Promise<unknown>;

public async create( ...args: Array<any> ): Promise<void> {
this.editor = await this._creator!( ...args );
}

/**
Expand All @@ -80,3 +78,5 @@ export class DisabledEditorWatchdog<TEditor extends Editor = Editor> {
}
}
}

export type EditorRelaxedCreatorFunction<TEditor = Editor> = ( ...args: any ) => Promise<TEditor>;
2 changes: 1 addition & 1 deletion src/ckeditor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"ckeditor 5"
],
"dependencies": {
"@ckeditor/ckeditor5-integrations-common": "^2.2.5"
"@ckeditor/ckeditor5-integrations-common": "^2.3.0"
},
"peerDependencies": {
"@angular/core": ">=19.2.19",
Expand Down
Loading