Skip to content
Open
7 changes: 7 additions & 0 deletions .changelog/20260525144322_ck_9935.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: Feature
---

Added support for the paragraph-like editor feature. It is now possible to customize the editable element's tag name, classes, styles, and attributes by passing `config.root.element` or `config.roots.main.element` through the integration.

The `tagName` property has been deprecated in favor of this new configuration.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@
"@angular/platform-browser": "^19.2.19",
"@angular/platform-browser-dynamic": "^19.2.19",
"@angular/router": "^19.2.19",
"@ckeditor/ckeditor5-integrations-common": "^2.3.1",
"@ckeditor/ckeditor5-integrations-common": "^2.4.0",
"core-js": "^3.21.1",
"rxjs": "^6.5.5",
"tslib": "^2.0.3",
"zone.js": "~0.15.1"
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "^2.2.3",
"@analogjs/vite-plugin-angular": "=2.3.1",
"@analogjs/vitest-angular": "^2.2.3",
"@angular-devkit/build-angular": "^19.2.19",
"@angular/cli": "^19.2.20",
Expand Down
6,240 changes: 2,838 additions & 3,402 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,12 @@ minimumReleaseAgeExclude:
shellEmulator: true
shamefullyHoist: true
preferFrozenLockfile: true
allowBuilds:
'@parcel/watcher': true
core-js: true
cypress: true
esbuild: true
lmdb: false
msgpackr-extract: false
protobufjs: true
snyk: false
8 changes: 5 additions & 3 deletions src/ckeditor/ckeditor.component.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import { AngularEditor } from 'src/editor/editor';
import { MockEditor } from 'src/editor/mock-editor';

import { CKEditorComponent } from './ckeditor.component';
import { EditorElementComponent } from './editor-element.component';

describe( 'CKEditorComponent integration', () => {
let component: CKEditorComponent;
let fixture: ComponentFixture<CKEditorComponent>;

beforeEach( async () => {
await TestBed.configureTestingModule( {
imports: [ EditorElementComponent ],
declarations: [ CKEditorComponent ]
} ).compileComponents();
} );
Expand All @@ -30,7 +32,7 @@ describe( 'CKEditorComponent integration', () => {
beforeEach( async () => {
fixture = TestBed.createComponent( CKEditorComponent );
component = fixture.componentInstance;
component.editor = AngularEditor;
component.editor = AngularEditor as any;
} );

afterEach( () => {
Expand Down Expand Up @@ -314,12 +316,12 @@ describe( 'CKEditorComponent integration', () => {

fixture = TestBed.createComponent( CKEditorComponent );
component = fixture.componentInstance;
component.editor = MockEditor;
component.editor = MockEditor as any;

const fixture2 = TestBed.createComponent( CKEditorComponent );
const component2 = fixture2.componentInstance;

component2.editor = MockEditor;
component2.editor = MockEditor as any;

window.onerror = null;

Expand Down
47 changes: 47 additions & 0 deletions src/ckeditor/ckeditor.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import { MockEditor } from 'src/editor/mock-editor';

import { CKEditorComponent } from './ckeditor.component';
import { AngularIntegrationUsageDataPlugin } from './plugins/angular-integration-usage-data.plugin';
import { EditorElementComponent } from './editor-element.component';

describe( 'CKEditorComponent', () => {
let component: CKEditorComponent;
let fixture: ComponentFixture<CKEditorComponent>;

beforeEach( async () => {
await TestBed.configureTestingModule( {
imports: [ EditorElementComponent ],
declarations: [ CKEditorComponent ]
} ).compileComponents();
} );
Expand Down Expand Up @@ -154,6 +156,51 @@ describe( 'CKEditorComponent', () => {
expect( component.getId() ).toMatch( /e[0-9a-z]{32}/ );
} );
} );

describe( 'elementDefinition', () => {
it( 'should return defaultTag if editor has no name or is ClassicEditor', () => {
component.editor = { ...MockEditor, editorName: undefined } as any;
component.tagName = 'section';
expect( component.elementDefinition ).toBe( 'section' );

component.editor = { ...MockEditor, editorName: 'ClassicEditor' } as any;
expect( component.elementDefinition ).toBe( 'section' );
} );

it( 'should return custom tag from config.roots.main.element if provided for non-classic editor', () => {
component.editor = { ...MockEditor, editorName: 'DecoupledEditor' } as any;
component.tagName = 'div';
component.config = {
roots: {
main: {
element: 'article'
}
}
} as any;

expect( component.elementDefinition ).toBe( 'article' );
} );

it( 'should return custom tag from config.root.element if config.roots.main.element is not provided', () => {
component.editor = { ...MockEditor, editorName: 'InlineEditor' } as any;
component.tagName = 'div';
component.config = {
root: {
element: 'aside'
}
} as any;

expect( component.elementDefinition ).toBe( 'aside' );
} );

it( 'should fallback to defaultTag if neither roots.main.element nor root.element is provided', () => {
component.editor = { ...MockEditor, editorName: 'InlineEditor' } as any;
component.tagName = 'span';
component.config = {} as any;

expect( component.elementDefinition ).toBe( 'span' );
} );
} );
} );
} );

Expand Down
63 changes: 46 additions & 17 deletions src/ckeditor/ckeditor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

import {
Component,
ElementRef,
EventEmitter,
forwardRef,
Inject,
Input,
NgZone,
Output,
ViewChild,
type AfterViewInit,
type OnChanges,
type OnDestroy,
Expand Down Expand Up @@ -40,12 +40,17 @@ import {
compareInstalledCKBaseVersion,
getInstalledCKBaseFeatures,
uid,
type EditorRelaxedConfig,
type EditorRelaxedConstructor
} from '@ckeditor/ckeditor5-integrations-common';
import { getLicenseKey } from './get-license-key';

import { getLicenseKey } from './utils/get-license-key';
import { appendAllIntegrationPluginsToConfig } from './plugins/append-all-integration-plugins-to-config';
import { DisabledEditorWatchdog, type EditorRelaxedCreatorFunction } from './disabled-editor-watchdog';

import type { EditorElementComponent } from './editor-element.component';
import type { EditorElementDefinition } from './utils/normalize-editor-element-definition';

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

export interface BlurEvent<TEditor extends Editor = Editor> {
Expand All @@ -65,7 +70,7 @@ export interface ChangeEvent<TEditor extends Editor = Editor> {

@Component( {
selector: 'ckeditor',
template: '<ng-template></ng-template>',
template: '<ckeditor-editor-element [definition]="elementDefinition" #editorEl />',
// Integration with @angular/forms.
providers: [
{
Expand All @@ -77,16 +82,11 @@ export interface ChangeEvent<TEditor extends Editor = Editor> {
standalone: false
} )
export class CKEditorComponent<TEditor extends Editor = Editor> implements AfterViewInit, OnDestroy, OnChanges, ControlValueAccessor {
/**
* The reference to the DOM element created by the component.
*/
private elementRef!: ElementRef<HTMLElement>;

/**
* 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?: EditorRelaxedConstructor<TEditor> & {
@Input( { required: true } ) public editor!: EditorRelaxedConstructor<TEditor> & {
EditorWatchdog: typeof EditorWatchdog;
};

Expand All @@ -109,9 +109,20 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
* Tag name of the editor component.
*
* The default tag is 'div'.
*
* @deprecated
*/
@Input() public tagName = 'div';

/**
* Returns the element definition derived from the current editor constructor
* and config. Used by the template to pass the correct definition down to
* editor element component.
*/
public get elementDefinition(): EditorElementDefinition {
return getEditorElementDefinition( this.editor, this.config, this.tagName );
}

/**
* The context watchdog.
*/
Expand Down Expand Up @@ -188,6 +199,13 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
*/
@Output() public error = new EventEmitter<unknown>();

/**
* Reference to the child component responsible for creating and managing
* the DOM element that the editor attaches to.
*/
@ViewChild( 'editorEl', { static: true } )
private editorElementComponent!: EditorElementComponent;

/**
* The instance of the editor created by this component.
*/
Expand Down Expand Up @@ -258,9 +276,8 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
return this.id;
}

public constructor( @Inject( ElementRef ) elementRef: ElementRef, @Inject( NgZone ) ngZone: NgZone ) {
public constructor( @Inject( NgZone ) ngZone: NgZone ) {
this.ngZone = ngZone;
this.elementRef = elementRef;

assertMinimumSupportedVersion();
}
Expand Down Expand Up @@ -364,15 +381,13 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
* because of the issue in the collaboration mode (#6).
*/
private attachToWatchdog() {
const Editor = this.editor!;
const Editor = this.editor;

const supports = getInstalledCKBaseFeatures();
const element = document.createElement( this.tagName );
const element = this.editorElementComponent.element!;
Comment thread
cursor[bot] marked this conversation as resolved.

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

const editor = await (
supports.elementConfigAttachment ?
Editor.create( assignElementToEditorConfig( Editor, element, config ) ) :
Expand All @@ -395,8 +410,6 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After

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

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

const emitError = ( e?: unknown ) => {
Expand Down Expand Up @@ -555,3 +568,19 @@ function assertMinimumSupportedVersion(): void {
function getEditorFromWatchdogOrNull( watchdog: EditorWatchdog | ContextWatchdog, id: string ) {
return ( watchdog as any )._watchdogs.get( id );
}

/**
* Get definition of the element used to create editor.
*/
function getEditorElementDefinition(
editor: EditorRelaxedConstructor,
config: EditorRelaxedConfig,
defaultTag: string
): EditorElementDefinition {
// Classic editor hides element rendered by React, so it makes no sense to use custom tag in this case.
if ( !editor.editorName || editor.editorName === 'ClassicEditor' ) {
return defaultTag;
}

return config.roots?.main?.element ?? config.root?.element ?? defaultTag;
Comment thread
cursor[bot] marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.
7 changes: 6 additions & 1 deletion src/ckeditor/ckeditor.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { CKEditorComponent } from './ckeditor.component';
import { EditorElementComponent } from './editor-element.component';

@NgModule( {
imports: [ FormsModule, CommonModule ],
imports: [
FormsModule,
CommonModule,
EditorElementComponent
],
declarations: [ CKEditorComponent ],
exports: [ CKEditorComponent ]
} )
Expand Down
Loading