UNPKG

@ckeditor/ckeditor5-angular

Version:

Official Angular component for CKEditor 5 – the best browser-based rich text editor.

488 lines (480 loc) 20.6 kB
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