• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ckeditor / ckeditor5-angular / 747

pending completion
747

cron

travis-ci-com

pomek
Release: v6.0.1.

38 of 38 branches covered (100.0%)

Branch coverage included in aggregate %.

157 of 157 relevant lines covered (100.0%)

25.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/ckeditor/ckeditor.component.ts
1
/**
2
 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
 * For licensing, see LICENSE.md.
4
 */
5

6
import type {
7
        AfterViewInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
8
import {
9
        Component,
10
        Input,
11
        Output,
12
        NgZone,
13
        EventEmitter,
14
        forwardRef,
15
        ElementRef
16
} from '@angular/core';
17

18
import { ContextWatchdog, EditorWatchdog } from '@ckeditor/ckeditor5-watchdog';
19
import { WatchdogConfig } from '@ckeditor/ckeditor5-watchdog/src/watchdog';
20
import { type Editor, EditorConfig } from '@ckeditor/ckeditor5-core';
21
import type { GetEventInfo } from '@ckeditor/ckeditor5-utils';
22
import type { DocumentChangeEvent } from '@ckeditor/ckeditor5-engine';
23
import type { ViewDocumentBlurEvent, ViewDocumentFocusEvent } from '@ckeditor/ckeditor5-engine/src/view/observer/focusobserver';
24
import { first } from 'rxjs/operators';
25

26
import uid from './uid';
27

28
import type {
29
        ControlValueAccessor } from '@angular/forms';
30
import {
31
        NG_VALUE_ACCESSOR
32
} from '@angular/forms';
33

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

36
export interface BlurEvent<TEditor extends Editor = Editor> {
37
        event: GetEventInfo<ViewDocumentBlurEvent>;
38
        editor: TEditor;
39
}
40

41
export interface FocusEvent<TEditor extends Editor = Editor> {
42
        event: GetEventInfo<ViewDocumentFocusEvent>;
43
        editor: TEditor;
44
}
45

46
export interface ChangeEvent<TEditor extends Editor = Editor> {
47
        event: GetEventInfo<DocumentChangeEvent>;
48
        editor: TEditor;
49
}
50

51
@Component( {
52
        selector: 'ckeditor',
53
        template: '<ng-template></ng-template>',
54

55
        // Integration with @angular/forms.
56
        providers: [
57
                {
58
                        provide: NG_VALUE_ACCESSOR,
59
                        // eslint-disable-next-line @typescript-eslint/no-use-before-define
60
                        useExisting: forwardRef( () => CKEditorComponent ),
8✔
61
                        multi: true
62
                }
63
        ]
64
} )
65
export class CKEditorComponent<TEditor extends Editor = Editor> implements AfterViewInit, OnDestroy, OnChanges, ControlValueAccessor {
1✔
66
        /**
67
         * The reference to the DOM element created by the component.
68
         */
69
        private elementRef!: ElementRef<HTMLElement>;
70

71
        /**
72
         * The constructor of the editor to be used for the instance of the component.
73
         * It can be e.g. the `ClassicEditorBuild`, `InlineEditorBuild` or some custom editor.
74
         */
75
        @Input() public editor?: { create( sourceElementOrData: HTMLElement | string, config?: EditorConfig ): Promise<TEditor> };
76

77
        /**
78
         * The configuration of the editor.
79
         * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html
80
         * to learn more.
81
         */
82
        @Input() public config: EditorConfig = {};
50✔
83

84
        /**
85
         * The initial data of the editor. Useful when not using the ngModel.
86
         * See https://angular.io/api/forms/NgModel to learn more.
87
         */
88
        @Input() public data = '';
50✔
89

90
        /**
91
         * Tag name of the editor component.
92
         *
93
         * The default tag is 'div'.
94
         */
95
        @Input() public tagName = 'div';
50✔
96

97
        // TODO Change to ContextWatchdog<Editor, HTMLElement> after new ckeditor5 alpha release
98
        /**
99
         * The context watchdog.
100
         */
101
        @Input() public watchdog?: ContextWatchdog;
102

103
        /**
104
         * Config for the EditorWatchdog.
105
         */
106
        @Input() public editorWatchdogConfig?: WatchdogConfig;
107

108
        /**
109
         * Allows disabling the two-way data binding mechanism. Disabling it can boost performance for large documents.
110
         *
111
         * When a component is connected using the [(ngModel)] or [formControl] directives and this value is set to true then none of the data
112
         * will ever be synchronized.
113
         *
114
         * An integrator must call `editor.data.get()` manually once the application needs the editor's data.
115
         * An editor instance can be received in the `ready()` callback.
116
         */
117
        @Input() public disableTwoWayDataBinding = false;
50✔
118

119
        /**
120
         * When set `true`, the editor becomes read-only.
121
         * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html#member-isReadOnly
122
         * to learn more.
123
         */
124
        @Input() public set disabled( isDisabled: boolean ) {
125
                this.setDisabledState( isDisabled );
15✔
126
        }
127

128
        public get disabled(): boolean {
129
                if ( this.editorInstance ) {
6✔
130
                        return this.editorInstance.isReadOnly;
4✔
131
                }
132

133
                return this.initiallyDisabled;
2✔
134
        }
135

136
        /**
137
         * Fires when the editor is ready. It corresponds with the `editor#ready`
138
         * https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html#event-ready
139
         * event.
140
         */
141
        @Output() public ready = new EventEmitter<TEditor>();
50✔
142

143
        /**
144
         * Fires when the content of the editor has changed. It corresponds with the `editor.model.document#change`
145
         * https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_document-Document.html#event-change
146
         * event.
147
         */
148
        @Output() public change = new EventEmitter<ChangeEvent<TEditor>>();
50✔
149

150
        /**
151
         * Fires when the editing view of the editor is blurred. It corresponds with the `editor.editing.view.document#blur`
152
         * https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_document-Document.html#event-event:blur
153
         * event.
154
         */
155
        @Output() public blur = new EventEmitter<BlurEvent<TEditor>>();
50✔
156

157
        /**
158
         * Fires when the editing view of the editor is focused. It corresponds with the `editor.editing.view.document#focus`
159
         * https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_document-Document.html#event-event:focus
160
         * event.
161
         */
162
        @Output() public focus = new EventEmitter<FocusEvent<TEditor>>();
50✔
163

164
        /**
165
         * Fires when the editor component crashes.
166
         */
167
        @Output() public error = new EventEmitter<void>();
50✔
168

169
        /**
170
         * The instance of the editor created by this component.
171
         */
172
        public get editorInstance(): TEditor | null {
173
                let editorWatchdog = this.editorWatchdog;
94✔
174

175
                if ( this.watchdog ) {
94✔
176
                        // Temporarily use the `_watchdogs` internal map as the `getItem()` method throws
177
                        // an error when the item is not registered yet.
178
                        // See https://github.com/ckeditor/ckeditor5-angular/issues/177.
179
                        // TODO should be able to change when new chages in Watcdog are released.
180
                        editorWatchdog = ( this.watchdog as any )._watchdogs.get( this.id );
5✔
181
                }
182

183
                if ( editorWatchdog ) {
94✔
184
                        return editorWatchdog.editor;
77✔
185
                }
186

187
                return null;
17✔
188
        }
189

190
        /**
191
         * The editor watchdog. It is created when the context watchdog is not passed to the component.
192
         * It keeps the editor running.
193
         */
194
        private editorWatchdog?: EditorWatchdog<TEditor>;
195

196
        /**
197
         * If the component is read–only before the editor instance is created, it remembers that state,
198
         * so the editor can become read–only once it is ready.
199
         */
200
        private initiallyDisabled = false;
50✔
201

202
        /**
203
         * An instance of https://angular.io/api/core/NgZone to allow the interaction with the editor
204
         * withing the Angular event loop.
205
         */
206
        private ngZone: NgZone;
207

208
        /**
209
         * A callback executed when the content of the editor changes. Part of the
210
         * `ControlValueAccessor` (https://angular.io/api/forms/ControlValueAccessor) interface.
211
         *
212
         * Note: Unset unless the component uses the `ngModel`.
213
         */
214
        private cvaOnChange?: ( data: string ) => void;
215

216
        /**
217
         * A callback executed when the editor has been blurred. Part of the
218
         * `ControlValueAccessor` (https://angular.io/api/forms/ControlValueAccessor) interface.
219
         *
220
         * Note: Unset unless the component uses the `ngModel`.
221
         */
222
        private cvaOnTouched?: () => void;
223

224
        /**
225
         * Reference to the source element used by the editor.
226
         */
227
        private editorElement?: HTMLElement;
228

229
        /**
230
         * A lock flag preventing from calling the `cvaOnChange()` during setting editor data.
231
         */
232
        private isEditorSettingData = false;
50✔
233

234
        private id = uid();
50✔
235

236
        public constructor( elementRef: ElementRef, ngZone: NgZone ) {
237
                this.ngZone = ngZone;
50✔
238
                this.elementRef = elementRef;
50✔
239

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

243
                if ( CKEDITOR_VERSION ) {
50✔
244
                        const [ major ] = CKEDITOR_VERSION.split( '.' ).map( Number );
49✔
245

246
                        if ( major < 37 ) {
49✔
247
                                console.warn( 'The <CKEditor> component requires using CKEditor 5 in version 37 or higher.' );
1✔
248
                        }
249
                } else {
250
                        console.warn( 'Cannot find the "CKEDITOR_VERSION" in the "window" scope.' );
1✔
251
                }
252
        }
253

254
        // Implementing the OnChanges interface. Whenever the `data` property is changed, update the editor content.
255
        public ngOnChanges( changes: SimpleChanges ): void {
256
                if ( Object.prototype.hasOwnProperty.call( changes, 'data' ) && changes.data && !changes.data.isFirstChange() ) {
22✔
257
                        this.writeValue( changes.data.currentValue );
2✔
258
                }
259
        }
260

261
        // Implementing the AfterViewInit interface.
262
        public ngAfterViewInit(): void {
263
                this.attachToWatchdog();
45✔
264
        }
265

266
        // Implementing the OnDestroy interface.
267
        public async ngOnDestroy(): Promise<void> {
268
                if ( this.watchdog ) {
50✔
269
                        await this.watchdog.remove( this.id );
4✔
270
                } else if ( this.editorWatchdog && this.editorWatchdog.editor ) {
46✔
271
                        await this.editorWatchdog.destroy();
25✔
272

273
                        this.editorWatchdog = undefined;
25✔
274
                }
275
        }
276

277
        // Implementing the ControlValueAccessor interface (only when binding to ngModel).
278
        public writeValue( value: string | null ): void {
279
                // This method is called with the `null` value when the form resets.
280
                // A component's responsibility is to restore to the initial state.
281
                if ( value === null ) {
22✔
282
                        value = '';
8✔
283
                }
284

285
                // If already initialized.
286
                if ( this.editorInstance ) {
22✔
287
                        // The lock mechanism prevents from calling `cvaOnChange()` during changing
288
                        // the editor state. See #139
289
                        this.isEditorSettingData = true;
6✔
290
                        this.editorInstance.data.set( value );
6✔
291
                        this.isEditorSettingData = false;
6✔
292
                }
293
                // If not, wait for it to be ready; store the data.
294
                else {
295
                        // If the editor element is already available, then update its content.
296
                        this.data = value;
16✔
297

298
                        // If not, then wait until it is ready
299
                        // and change data only for the first `ready` event.
300
                        this.ready
16✔
301
                                .pipe( first() )
302
                                .subscribe( editor => {
303
                                        editor.data.set( this.data );
16✔
304
                                } );
305
                }
306
        }
307

308
        // Implementing the ControlValueAccessor interface (only when binding to ngModel).
309
        public registerOnChange( callback: ( data: string ) => void ): void {
310
                this.cvaOnChange = callback;
14✔
311
        }
312

313
        // Implementing the ControlValueAccessor interface (only when binding to ngModel).
314
        public registerOnTouched( callback: () => void ): void {
315
                this.cvaOnTouched = callback;
13✔
316
        }
317

318
        // Implementing the ControlValueAccessor interface (only when binding to ngModel).
319
        public setDisabledState( isDisabled: boolean ): void {
320
                // If already initialized.
321
                if ( this.editorInstance ) {
16✔
322
                        if ( isDisabled ) {
2✔
323
                                this.editorInstance.enableReadOnlyMode( ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID );
1✔
324
                        } else {
325
                                this.editorInstance.disableReadOnlyMode( ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID );
1✔
326
                        }
327
                }
328

329
                // Store the state anyway to use it once the editor is created.
330
                this.initiallyDisabled = isDisabled;
16✔
331
        }
332

333
        /**
334
         * Creates the editor instance, sets initial editor data, then integrates
335
         * the editor with the Angular component. This method does not use the `editor.data.set()`
336
         * because of the issue in the collaboration mode (#6).
337
         */
338
        private attachToWatchdog() {
339
                // TODO: elementOrData parameter type can be simplified to HTMLElemen after templated Watchdog will be released.
340
                const creator = ( ( elementOrData: HTMLElement | string | Record<string, string>, config: EditorConfig ) => {
45✔
341
                        return this.ngZone.runOutsideAngular( async () => {
47✔
342
                                this.elementRef.nativeElement.appendChild( elementOrData as HTMLElement );
47✔
343

344
                                const editor = await this.editor!.create( elementOrData as HTMLElement, config );
47✔
345

346
                                if ( this.initiallyDisabled ) {
47✔
347
                                        editor.enableReadOnlyMode( ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID );
2✔
348
                                }
349

350
                                this.ngZone.run( () => {
47✔
351
                                        this.ready.emit( editor );
47✔
352
                                } );
353

354
                                this.setUpEditorEvents( editor );
47✔
355

356
                                return editor;
47✔
357
                        } );
358
                } );
359

360
                const destructor = async ( editor: Editor ) => {
45✔
361
                        await editor.destroy();
32✔
362

363
                        this.elementRef.nativeElement.removeChild( this.editorElement! );
32✔
364
                };
365

366
                const emitError = () => {
45✔
367
                        this.ngZone.run( () => {
3✔
368
                                this.error.emit();
3✔
369
                        } );
370
                };
371

372
                const element = document.createElement( this.tagName );
45✔
373
                const config = this.getConfig();
45✔
374

375
                this.editorElement = element;
44✔
376

377
                // Based on the presence of the watchdog decide how to initialize the editor.
378
                if ( this.watchdog ) {
44✔
379
                        // When the context watchdog is passed add the new item to it based on the passed configuration.
380
                        this.watchdog.add( {
4✔
381
                                id: this.id,
382
                                type: 'editor',
383
                                creator,
384
                                destructor,
385
                                sourceElementOrData: element,
386
                                config
387
                        } );
388

389
                        this.watchdog.on( 'itemError', ( _, { itemId } ) => {
4✔
390
                                if ( itemId === this.id ) {
2✔
391
                                        emitError();
1✔
392
                                }
393
                        } );
394
                } else {
395
                        // In the other case create the watchdog by hand to keep the editor running.
396
                        const editorWatchdog = new EditorWatchdog(
40✔
397
                                this.editor!,
398
                                this.editorWatchdogConfig );
399

400
                        editorWatchdog.setCreator( creator );
40✔
401
                        editorWatchdog.setDestructor( destructor );
40✔
402
                        editorWatchdog.on( 'error', emitError );
40✔
403

404
                        this.editorWatchdog = editorWatchdog;
40✔
405

406
                        this.editorWatchdog.create( element, config );
40✔
407
                }
408
        }
409

410
        private getConfig() {
411
                if ( this.data && this.config.initialData ) {
45✔
412
                        throw new Error( 'Editor data should be provided either using `config.initialData` or `data` properties.' );
1✔
413
                }
414

415
                const config = { ...this.config };
44✔
416

417
                // Merge two possible ways of providing data into the `config.initialData` field.
418
                const initialData = this.config.initialData || this.data;
44✔
419

420
                if ( initialData ) {
44✔
421
                        // Define the `config.initialData` only when the initial content is specified.
422
                        config.initialData = initialData;
18✔
423
                }
424

425
                return config;
44✔
426
        }
427

428
        /**
429
         * Integrates the editor with the component by attaching related event listeners.
430
         */
431
        private setUpEditorEvents( editor: TEditor ): void {
432
                const modelDocument = editor.model.document;
47✔
433
                const viewDocument = editor.editing.view.document;
47✔
434

435
                modelDocument.on<DocumentChangeEvent>( 'change:data', evt => {
47✔
436
                        this.ngZone.run( () => {
14✔
437
                                if ( this.disableTwoWayDataBinding ) {
14✔
438
                                        return;
1✔
439
                                }
440

441
                                if ( this.cvaOnChange && !this.isEditorSettingData ) {
13✔
442
                                        const data = editor.data.get();
3✔
443

444
                                        this.cvaOnChange( data );
3✔
445
                                }
446

447
                                this.change.emit( { event: evt, editor } );
13✔
448
                        } );
449
                } );
450

451
                viewDocument.on<ViewDocumentFocusEvent>( 'focus', evt => {
47✔
452
                        this.ngZone.run( () => {
3✔
453
                                this.focus.emit( { event: evt, editor } );
3✔
454
                        } );
455
                } );
456

457
                viewDocument.on<ViewDocumentBlurEvent>( 'blur', evt => {
47✔
458
                        this.ngZone.run( () => {
4✔
459
                                if ( this.cvaOnTouched ) {
4✔
460
                                        this.cvaOnTouched();
2✔
461
                                }
462

463
                                this.blur.emit( { event: evt, editor } );
4✔
464
                        } );
465
                } );
466
        }
467
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc