ckeditor4-angular
Version:
Official CKEditor 4 component for Angular.
428 lines (422 loc) • 17.4 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, forwardRef, Component, Input, Output, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
import { getEditorNamespace } from 'ckeditor4-integrations-common';
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
class CKEditorComponent {
constructor(elementRef, ngZone) {
this.elementRef = elementRef;
this.ngZone = ngZone;
/**
* CKEditor 4 script url address. Script will be loaded only if CKEDITOR namespace is missing.
*
* Defaults to 'https://cdn.ckeditor.com/4.25.1-lts/standard-all/ckeditor.js'
*/
this.editorUrl = 'https://cdn.ckeditor.com/4.25.1-lts/standard-all/ckeditor.js';
/**
* Tag name of the editor component.
*
* The default tag is `textarea`.
*/
this.tagName = 'textarea';
/**
* The type of the editor interface.
*
* By default editor interface will be initialized as `classic` editor.
* You can also choose to create an editor with `inline` interface type instead.
*
* See https://ckeditor.com/docs/ckeditor4/latest/guide/dev_uitypes.html
* and https://ckeditor.com/docs/ckeditor4/latest/examples/fixedui.html
* to learn more.
*/
this.type = "classic" /* CLASSIC */;
/**
* Fired when the CKEDITOR https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html namespace
* is loaded. It only triggers once, no matter how many CKEditor 4 components are initialised.
* Can be used for convenient changes in the namespace, e.g. for adding external plugins.
*/
this.namespaceLoaded = new EventEmitter();
/**
* Fires when the editor is ready. It corresponds with the `editor#instanceReady`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-instanceReady
* event.
*/
this.ready = new EventEmitter();
/**
* Fires when the editor data is loaded, e.g. after calling setData()
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#method-setData
* editor's method. It corresponds with the `editor#dataReady`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dataReady event.
*/
this.dataReady = new EventEmitter();
/**
* Fires when the content of the editor has changed. It corresponds with the `editor#change`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-change
* event. For performance reasons this event may be called even when data didn't really changed.
* Please note that this event will only be fired when `undo` plugin is loaded. If you need to
* listen for editor changes (e.g. for two-way data binding), use `dataChange` event instead.
*/
this.change = new EventEmitter();
/**
* Fires when the content of the editor has changed. In contrast to `change` - only emits when
* data really changed thus can be successfully used with `[data]` and two way `[(data)]` binding.
*
* See more: https://angular.io/guide/template-syntax#two-way-binding---
*/
this.dataChange = new EventEmitter();
/**
* Fires when the native dragStart event occurs. It corresponds with the `editor#dragstart`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dragstart
* event.
*/
this.dragStart = new EventEmitter();
/**
* Fires when the native dragEnd event occurs. It corresponds with the `editor#dragend`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dragend
* event.
*/
this.dragEnd = new EventEmitter();
/**
* Fires when the native drop event occurs. It corresponds with the `editor#drop`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-drop
* event.
*/
this.drop = new EventEmitter();
/**
* Fires when the file loader response is received. It corresponds with the `editor#fileUploadResponse`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-fileUploadResponse
* event.
*/
this.fileUploadResponse = new EventEmitter();
/**
* Fires when the file loader should send XHR. It corresponds with the `editor#fileUploadRequest`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-fileUploadRequest
* event.
*/
this.fileUploadRequest = new EventEmitter();
/**
* Fires when the editing area of the editor is focused. It corresponds with the `editor#focus`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-focus
* event.
*/
this.focus = new EventEmitter();
/**
* Fires after the user initiated a paste action, but before the data is inserted.
* It corresponds with the `editor#paste`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-paste
* event.
*/
this.paste = new EventEmitter();
/**
* Fires after the `paste` event if content was modified. It corresponds with the `editor#afterPaste`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-afterPaste
* event.
*/
this.afterPaste = new EventEmitter();
/**
* Fires when the editing view of the editor is blurred. It corresponds with the `editor#blur`
* https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-blur
* event.
*/
this.blur = new EventEmitter();
/**
* 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.
*/
this._readOnly = null;
this._data = null;
this._destroyed = false;
}
/**
* Keeps track of the editor's data.
*
* It's also decorated as an input which is useful when not using the ngModel.
*
* See https://angular.io/api/forms/NgModel to learn more.
*/
set data(data) {
if (data === this._data) {
return;
}
if (this.instance) {
this.instance.setData(data);
// Data may be changed by ACF.
this._data = this.instance.getData();
return;
}
this._data = data;
}
get data() {
return this._data;
}
/**
* When set to `true`, the editor becomes read-only.
*
* See https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#property-readOnly
* to learn more.
*/
set readOnly(isReadOnly) {
if (this.instance) {
this.instance.setReadOnly(isReadOnly);
return;
}
// Delay setting read-only state until editor initialization.
this._readOnly = isReadOnly;
}
get readOnly() {
if (this.instance) {
return this.instance.readOnly;
}
return this._readOnly;
}
ngAfterViewInit() {
getEditorNamespace(this.editorUrl, namespace => {
this.namespaceLoaded.emit(namespace);
}).then(() => {
// Check if component instance was destroyed before `ngAfterViewInit` call (#110).
// Here, `this.instance` is still not initialized and so additional flag is needed.
if (this._destroyed) {
return;
}
this.ngZone.runOutsideAngular(this.createEditor.bind(this));
}).catch(window.console.error);
}
ngOnDestroy() {
this._destroyed = true;
this.ngZone.runOutsideAngular(() => {
if (this.instance) {
this.instance.destroy();
this.instance = null;
}
});
}
writeValue(value) {
this.data = value;
}
registerOnChange(callback) {
this.onChange = callback;
}
registerOnTouched(callback) {
this.onTouched = callback;
}
createEditor() {
const element = document.createElement(this.tagName);
this.elementRef.nativeElement.appendChild(element);
const userInstanceReadyCallback = this.config?.on?.instanceReady;
const defaultConfig = {
delayIfDetached: true
};
const config = { ...defaultConfig, ...this.config };
if (typeof config.on === 'undefined') {
config.on = {};
}
config.on.instanceReady = evt => {
const editor = evt.editor;
this.instance = editor;
// Read only state may change during instance initialization.
this.readOnly = this._readOnly !== null ? this._readOnly : this.instance.readOnly;
this.subscribe(this.instance);
const undo = editor.undoManager;
if (this.data !== null) {
undo && undo.lock();
editor.setData(this.data, { callback: () => {
// Locking undoManager prevents 'change' event.
// Trigger it manually to updated bound data.
if (this.data !== editor.getData()) {
undo ? editor.fire('change') : editor.fire('dataReady');
}
undo && undo.unlock();
this.ngZone.run(() => {
if (typeof userInstanceReadyCallback === 'function') {
userInstanceReadyCallback(evt);
}
this.ready.emit(evt);
});
} });
}
else {
this.ngZone.run(() => {
if (typeof userInstanceReadyCallback === 'function') {
userInstanceReadyCallback(evt);
}
this.ready.emit(evt);
});
}
};
if (this.type === "inline" /* INLINE */) {
CKEDITOR.inline(element, config);
}
else {
CKEDITOR.replace(element, config);
}
}
subscribe(editor) {
editor.on('focus', evt => {
this.ngZone.run(() => {
this.focus.emit(evt);
});
});
editor.on('paste', evt => {
this.ngZone.run(() => {
this.paste.emit(evt);
});
});
editor.on('afterPaste', evt => {
this.ngZone.run(() => {
this.afterPaste.emit(evt);
});
});
editor.on('dragend', evt => {
this.ngZone.run(() => {
this.dragEnd.emit(evt);
});
});
editor.on('dragstart', evt => {
this.ngZone.run(() => {
this.dragStart.emit(evt);
});
});
editor.on('drop', evt => {
this.ngZone.run(() => {
this.drop.emit(evt);
});
});
editor.on('fileUploadRequest', evt => {
this.ngZone.run(() => {
this.fileUploadRequest.emit(evt);
});
});
editor.on('fileUploadResponse', evt => {
this.ngZone.run(() => {
this.fileUploadResponse.emit(evt);
});
});
editor.on('blur', evt => {
this.ngZone.run(() => {
if (this.onTouched) {
this.onTouched();
}
this.blur.emit(evt);
});
});
editor.on('dataReady', this.propagateChange, this);
if (this.instance.undoManager) {
editor.on('change', this.propagateChange, this);
}
// If 'undo' plugin is not loaded, listen to 'selectionCheck' event instead. (#54).
else {
editor.on('selectionCheck', this.propagateChange, this);
}
}
propagateChange(event) {
this.ngZone.run(() => {
const newData = this.instance.getData();
if (event.name === 'change') {
this.change.emit(event);
}
else if (event.name === 'dataReady') {
this.dataReady.emit(event);
}
if (newData === this.data) {
return;
}
this._data = newData;
this.dataChange.emit(newData);
if (this.onChange) {
this.onChange(newData);
}
});
}
}
CKEditorComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: CKEditorComponent, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
CKEditorComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: CKEditorComponent, selector: "ckeditor", inputs: { config: "config", editorUrl: "editorUrl", tagName: "tagName", type: "type", data: "data", readOnly: "readOnly" }, outputs: { namespaceLoaded: "namespaceLoaded", ready: "ready", dataReady: "dataReady", change: "change", dataChange: "dataChange", dragStart: "dragStart", dragEnd: "dragEnd", drop: "drop", fileUploadResponse: "fileUploadResponse", fileUploadRequest: "fileUploadRequest", focus: "focus", paste: "paste", afterPaste: "afterPaste", blur: "blur" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CKEditorComponent),
multi: true,
}
], ngImport: i0, template: '<ng-template></ng-template>', isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: CKEditorComponent, decorators: [{
type: Component,
args: [{
selector: 'ckeditor',
template: '<ng-template></ng-template>',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CKEditorComponent),
multi: true,
}
]
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.NgZone }]; }, propDecorators: { config: [{
type: Input
}], editorUrl: [{
type: Input
}], tagName: [{
type: Input
}], type: [{
type: Input
}], data: [{
type: Input
}], readOnly: [{
type: Input
}], namespaceLoaded: [{
type: Output
}], ready: [{
type: Output
}], dataReady: [{
type: Output
}], change: [{
type: Output
}], dataChange: [{
type: Output
}], dragStart: [{
type: Output
}], dragEnd: [{
type: Output
}], drop: [{
type: Output
}], fileUploadResponse: [{
type: Output
}], fileUploadRequest: [{
type: Output
}], focus: [{
type: Output
}], paste: [{
type: Output
}], afterPaste: [{
type: Output
}], blur: [{
type: Output
}] } });
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
class CKEditorModule {
}
CKEditorModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: CKEditorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
CKEditorModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: CKEditorModule, declarations: [CKEditorComponent], imports: [FormsModule, CommonModule], exports: [CKEditorComponent] });
CKEditorModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: CKEditorModule, imports: [[FormsModule, CommonModule]] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: CKEditorModule, decorators: [{
type: NgModule,
args: [{
imports: [FormsModule, CommonModule],
declarations: [CKEditorComponent],
exports: [CKEditorComponent]
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { CKEditorComponent, CKEditorModule };
//# sourceMappingURL=ckeditor4-angular.mjs.map