@ckeditor/ckeditor5-angular
Version:
Official Angular component for CKEditor 5 – the best browser-based rich text editor.
488 lines (480 loc) • 20.6 kB
JavaScript
import * as i0 from '@angular/core';
import { VERSION, EventEmitter, forwardRef, Component, Input, Output, NgModule } from '@angular/core';
import { first } from 'rxjs/operators';
import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
import { createIntegrationUsageDataPlugin, isCKEditorFreeLicense, appendExtraPluginsToEditorConfig, uid } from '@ckeditor/ckeditor5-integrations-common';
export { loadCKEditorCloud } from '@ckeditor/ckeditor5-integrations-common';
import { CommonModule } from '@angular/common';
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* This part of the code is not executed in open-source implementations using a GPL key.
* It only runs when a specific license key is provided. If you are uncertain whether
* this applies to your installation, please contact our support team.
*/
const AngularIntegrationUsageDataPlugin = createIntegrationUsageDataPlugin('angular', {
version: /* replace-version:start */ '9.1.0' /* replace-version:end */,
frameworkVersion: VERSION.full
});
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Appends all integration plugins to the editor configuration.
*
* @param editorConfig The editor configuration.
* @returns The editor configuration with all integration plugins appended.
*/
function appendAllIntegrationPluginsToConfig(editorConfig) {
const extraPlugins = [];
if (!isCKEditorFreeLicense(editorConfig.licenseKey)) {
/**
* This part of the code is not executed in open-source implementations using a GPL key.
* It only runs when a specific license key is provided. If you are uncertain whether
* this applies to your installation, please contact our support team.
*/
extraPlugins.push(AngularIntegrationUsageDataPlugin);
}
return appendExtraPluginsToEditorConfig(editorConfig, extraPlugins);
}
const ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID = 'Lock from Angular integration (@ckeditor/ckeditor5-angular)';
class CKEditorComponent {
/**
* The reference to the DOM element created by the component.
*/
elementRef;
/**
* 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.
*/
editor;
/**
* The configuration of the editor.
* See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html
* to learn more.
*/
config = {};
/**
* The initial data of the editor. Useful when not using the ngModel.
* See https://angular.io/api/forms/NgModel to learn more.
*/
data = '';
/**
* Tag name of the editor component.
*
* The default tag is 'div'.
*/
tagName = 'div';
// TODO Change to ContextWatchdog<Editor, HTMLElement> after new ckeditor5 alpha release
/**
* The context watchdog.
*/
watchdog;
/**
* Config for the EditorWatchdog.
*/
editorWatchdogConfig;
/**
* Allows disabling the two-way data binding mechanism. Disabling it can boost performance for large documents.
*
* When a component is connected using the [(ngModel)] or [formControl] directives and this value is set to true then none of the data
* will ever be synchronized.
*
* An integrator must call `editor.data.get()` manually once the application needs the editor's data.
* An editor instance can be received in the `ready()` callback.
*/
disableTwoWayDataBinding = false;
/**
* When set `true`, the editor becomes read-only.
* See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html#member-isReadOnly
* to learn more.
*/
set disabled(isDisabled) {
this.setDisabledState(isDisabled);
}
get disabled() {
if (this.editorInstance) {
return this.editorInstance.isReadOnly;
}
return this.initiallyDisabled;
}
/**
* Fires when the editor is ready. It corresponds with the `editor#ready`
* https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html#event-ready
* event.
*/
ready = new EventEmitter();
/**
* Fires when the content of the editor has changed. It corresponds with the `editor.model.document#change`
* https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_document-Document.html#event-change
* event.
*/
change = new EventEmitter();
/**
* Fires when the editing view of the editor is blurred. It corresponds with the `editor.editing.view.document#blur`
* https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_document-Document.html#event-event:blur
* event.
*/
blur = new EventEmitter();
/**
* Fires when the editing view of the editor is focused. It corresponds with the `editor.editing.view.document#focus`
* https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_document-Document.html#event-event:focus
* event.
*/
focus = new EventEmitter();
/**
* Fires when the editor component crashes.
*/
error = new EventEmitter();
/**
* The instance of the editor created by this component.
*/
get editorInstance() {
let editorWatchdog = this.editorWatchdog;
if (this.watchdog) {
// Temporarily use the `_watchdogs` internal map as the `getItem()` method throws
// an error when the item is not registered yet.
// See https://github.com/ckeditor/ckeditor5-angular/issues/177.
// TODO should be able to change when new chages in Watcdog are released.
editorWatchdog = this.watchdog._watchdogs.get(this.id);
}
if (editorWatchdog) {
return editorWatchdog.editor;
}
return null;
}
/**
* The editor watchdog. It is created when the context watchdog is not passed to the component.
* It keeps the editor running.
*/
editorWatchdog;
/**
* If the component is read–only before the editor instance is created, it remembers that state,
* so the editor can become read–only once it is ready.
*/
initiallyDisabled = false;
/**
* An instance of https://angular.io/api/core/NgZone to allow the interaction with the editor
* withing the Angular event loop.
*/
ngZone;
/**
* A callback executed when the content of the editor changes. Part of the
* `ControlValueAccessor` (https://angular.io/api/forms/ControlValueAccessor) interface.
*
* Note: Unset unless the component uses the `ngModel`.
*/
cvaOnChange;
/**
* A callback executed when the editor has been blurred. Part of the
* `ControlValueAccessor` (https://angular.io/api/forms/ControlValueAccessor) interface.
*
* Note: Unset unless the component uses the `ngModel`.
*/
cvaOnTouched;
/**
* Reference to the source element used by the editor.
*/
editorElement;
/**
* A lock flag preventing from calling the `cvaOnChange()` during setting editor data.
*/
isEditorSettingData = false;
id = uid();
getId() {
return this.id;
}
constructor(elementRef, ngZone) {
this.ngZone = ngZone;
this.elementRef = elementRef;
this.checkVersion();
}
checkVersion() {
// To avoid issues with the community typings and CKEditor 5, let's treat window as any. See #342.
const { CKEDITOR_VERSION } = window;
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.');
}
// Implementing the OnChanges interface. Whenever the `data` property is changed, update the editor content.
ngOnChanges(changes) {
if (Object.prototype.hasOwnProperty.call(changes, 'data') && changes.data && !changes.data.isFirstChange()) {
this.writeValue(changes.data.currentValue);
}
}
// Implementing the AfterViewInit interface.
ngAfterViewInit() {
this.attachToWatchdog();
}
// Implementing the OnDestroy interface.
async ngOnDestroy() {
if (this.watchdog) {
await this.watchdog.remove(this.id);
}
else if (this.editorWatchdog && this.editorWatchdog.editor) {
await this.editorWatchdog.destroy();
this.editorWatchdog = undefined;
}
}
// Implementing the ControlValueAccessor interface (only when binding to ngModel).
writeValue(value) {
// This method is called with the `null` value when the form resets.
// A component's responsibility is to restore to the initial state.
if (value === null) {
value = '';
}
// If already initialized.
if (this.editorInstance) {
// The lock mechanism prevents from calling `cvaOnChange()` during changing
// the editor state. See #139
this.isEditorSettingData = true;
this.editorInstance.data.set(value);
this.isEditorSettingData = false;
}
// If not, wait for it to be ready; store the data.
else {
// If the editor element is already available, then update its content.
this.data = value;
// If not, then wait until it is ready
// and change data only for the first `ready` event.
this.ready
.pipe(first())
.subscribe(editor => {
editor.data.set(this.data);
});
}
}
// Implementing the ControlValueAccessor interface (only when binding to ngModel).
registerOnChange(callback) {
this.cvaOnChange = callback;
}
// Implementing the ControlValueAccessor interface (only when binding to ngModel).
registerOnTouched(callback) {
this.cvaOnTouched = callback;
}
// Implementing the ControlValueAccessor interface (only when binding to ngModel).
setDisabledState(isDisabled) {
// If already initialized.
if (this.editorInstance) {
if (isDisabled) {
this.editorInstance.enableReadOnlyMode(ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID);
}
else {
this.editorInstance.disableReadOnlyMode(ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID);
}
}
// Store the state anyway to use it once the editor is created.
this.initiallyDisabled = isDisabled;
}
/**
* Creates the editor instance, sets initial editor data, then integrates
* the editor with the Angular component. This method does not use the `editor.data.set()`
* because of the issue in the collaboration mode (#6).
*/
attachToWatchdog() {
// TODO: elementOrData parameter type can be simplified to HTMLElemen after templated Watchdog will be released.
const creator = ((elementOrData, config) => {
return this.ngZone.runOutsideAngular(async () => {
this.elementRef.nativeElement.appendChild(elementOrData);
const editor = await this.editor.create(elementOrData, config);
if (this.initiallyDisabled) {
editor.enableReadOnlyMode(ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID);
}
this.ngZone.run(() => {
this.ready.emit(editor);
});
this.setUpEditorEvents(editor);
return editor;
});
});
const destructor = async (editor) => {
await editor.destroy();
this.elementRef.nativeElement.removeChild(this.editorElement);
};
const emitError = (e) => {
// Do not run change detection by re-entering the Angular zone if the `error`
// emitter doesn't have any subscribers.
// Subscribers are pushed onto the list whenever `error` is listened inside the template:
// `<ckeditor (error)="onError(...)"></ckeditor>`.
if (hasObservers(this.error)) {
this.ngZone.run(() => this.error.emit(e));
}
else {
// Print error to the console when there are no subscribers to the `error` event.
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) {
// When the context watchdog is passed add the new item to it based on the passed configuration.
this.watchdog.add({
id: this.id,
type: 'editor',
creator,
destructor,
sourceElementOrData: element,
config
}).catch(e => {
emitError(e);
});
this.watchdog.on('itemError', (_, { itemId }) => {
if (itemId === this.id) {
emitError();
}
});
}
else {
// In the other case create the watchdog by hand to keep the editor running.
const editorWatchdog = new this.editor.EditorWatchdog(this.editor, this.editorWatchdogConfig);
editorWatchdog.setCreator(creator);
editorWatchdog.setDestructor(destructor);
editorWatchdog.on('error', emitError);
this.editorWatchdog = editorWatchdog;
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 => {
emitError(e);
});
});
}
}
getConfig() {
if (this.data && this.config.initialData) {
throw new Error('Editor data should be provided either using `config.initialData` or `data` properties.');
}
const config = { ...this.config };
// Merge two possible ways of providing data into the `config.initialData` field.
const initialData = this.config.initialData || this.data;
if (initialData) {
// Define the `config.initialData` only when the initial content is specified.
config.initialData = initialData;
}
return appendAllIntegrationPluginsToConfig(config);
}
/**
* Integrates the editor with the component by attaching related event listeners.
*/
setUpEditorEvents(editor) {
const modelDocument = editor.model.document;
const viewDocument = editor.editing.view.document;
modelDocument.on('change:data', evt => {
this.ngZone.run(() => {
if (this.disableTwoWayDataBinding) {
return;
}
if (this.cvaOnChange && !this.isEditorSettingData) {
const data = editor.data.get();
this.cvaOnChange(data);
}
this.change.emit({ event: evt, editor });
});
});
viewDocument.on('focus', evt => {
this.ngZone.run(() => {
this.focus.emit({ event: evt, editor });
});
});
viewDocument.on('blur', evt => {
this.ngZone.run(() => {
if (this.cvaOnTouched) {
this.cvaOnTouched();
}
this.blur.emit({ event: evt, editor });
});
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: CKEditorComponent, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: CKEditorComponent, selector: "ckeditor", inputs: { editor: "editor", config: "config", data: "data", tagName: "tagName", watchdog: "watchdog", editorWatchdogConfig: "editorWatchdogConfig", disableTwoWayDataBinding: "disableTwoWayDataBinding", disabled: "disabled" }, outputs: { ready: "ready", change: "change", blur: "blur", focus: "focus", error: "error" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
// eslint-disable-next-line @typescript-eslint/no-use-before-define
useExisting: forwardRef(() => CKEditorComponent),
multi: true
}
], usesOnChanges: true, ngImport: i0, template: '<ng-template></ng-template>', isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: CKEditorComponent, decorators: [{
type: Component,
args: [{
selector: 'ckeditor',
template: '<ng-template></ng-template>',
// Integration with @angular/forms.
providers: [
{
provide: NG_VALUE_ACCESSOR,
// eslint-disable-next-line @typescript-eslint/no-use-before-define
useExisting: forwardRef(() => CKEditorComponent),
multi: true
}
]
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.NgZone }]; }, propDecorators: { editor: [{
type: Input
}], config: [{
type: Input
}], data: [{
type: Input
}], tagName: [{
type: Input
}], watchdog: [{
type: Input
}], editorWatchdogConfig: [{
type: Input
}], disableTwoWayDataBinding: [{
type: Input
}], disabled: [{
type: Input
}], ready: [{
type: Output
}], change: [{
type: Output
}], blur: [{
type: Output
}], focus: [{
type: Output
}], error: [{
type: Output
}] } });
function hasObservers(emitter) {
// Cast to `any` because `observed` property is available in RxJS >= 7.2.0.
// Fallback to checking `observers` list if this property is not defined.
return emitter.observed || emitter.observers.length > 0;
}
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
class CKEditorModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: CKEditorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.2.12", ngImport: i0, type: CKEditorModule, declarations: [CKEditorComponent], imports: [FormsModule, CommonModule], exports: [CKEditorComponent] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: CKEditorModule, imports: [FormsModule, CommonModule] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: CKEditorModule, decorators: [{
type: NgModule,
args: [{
imports: [FormsModule, CommonModule],
declarations: [CKEditorComponent],
exports: [CKEditorComponent]
}]
}] });
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Generated bundle index. Do not edit.
*/
export { CKEditorComponent, CKEditorModule };
//# sourceMappingURL=ckeditor-ckeditor5-angular.mjs.map