Skip to content

Commit

Permalink
Merge pull request #17321 from bendemboski/optimize-body-wrapper-lookup
Browse files Browse the repository at this point in the history
Other (ui): Improved the performance of the `BodyCollection` DOM wrapper lookup by replacing `document.querySelector()` with a static element reference.

Huge thanks to [Ben Demboski](https://github.com/bendemboski) for this contribution!
  • Loading branch information
oleq authored Nov 14, 2024
2 parents 7f3f02e + d79ed9b commit ce735cc
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 22 deletions.
77 changes: 55 additions & 22 deletions packages/ckeditor5-ui/src/editorui/bodycollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,51 @@ import type View from '../view.js';
import { createElement, type Locale } from '@ckeditor/ckeditor5-utils';

/**
* This is a special {@link module:ui/viewcollection~ViewCollection} dedicated to elements that are detached
* from the DOM structure of the editor, like panels, icons, etc.
* This is a special {@link module:ui/viewcollection~ViewCollection} dedicated to elements that are detached from the DOM structure of
* the editor, like floating panels, floating toolbars, dialogs, etc.
*
* The body collection is available in the {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} property.
* The body collection is available under the {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} property.
* Any plugin can add a {@link module:ui/view~View view} to this collection.
* These views will render in a container placed directly in the `<body>` element.
* The editor will detach and destroy this collection when the editor will be {@link module:core/editor/editor~Editor#destroy destroyed}.
*
* If you need to control the life cycle of the body collection on your own, you can create your own instance of this class.
* All views added to a body collection render in a dedicated DOM container (`<div class="ck ck-body ...">...</div>`). All body collection
* containers render in a common shared (`<div class="ck-body-wrapper">...</div>`) in the DOM to limit the pollution of
* the `<body>` element. The resulting DOM structure is as follows:
*
* A body collection will render itself automatically in the DOM body element as soon as you call {@link ~BodyCollection#attachToDom}.
* If you create multiple body collections, this class will create a special wrapper element in the DOM to limit the number of
* elements created directly in the body and remove it when the last body collection will be
* {@link ~BodyCollection#detachFromDom detached}.
* ```html
* <body>
* <!-- Content of the webpage... -->
*
* <!-- The shared wrapper for all body collection containers. -->
* <div class="ck-body-wrapper">
* <!-- The container of the first body collection instance. -->
* <div class="ck ck-body ...">
* <!-- View elements belonging to the first body collection -->
* </div>
*
* <!-- The container of the second body collection instance. -->
* <div class="ck ck-body ...">...</div>
*
* <!-- More body collection containers for the rest of instances... -->
* </div>
* </body>
* ```
*
* By default, the {@link module:ui/editorui/editoruiview~EditorUIView `editor.ui.view`} manages the life cycle of the
* {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} collection, attaching and detaching it
* when the editor gets created or {@link module:core/editor/editor~Editor#destroy destroyed}.
*
* # Custom body collection instances
*
* Even though most editor instances come with a built-in body collection
* ({@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`}), you can create your own instance of this
* class if you need to control their life cycle.
*
* The life cycle of a custom body collection must be handled manually by the developer using the dedicated API:
* * A body collection will render itself automatically in the DOM as soon as you call {@link ~BodyCollection#attachToDom}.
* * Calling {@link ~BodyCollection#detachFromDom} will remove the collection from the DOM.
*
* **Note**: The shared collection wrapper (`<div class="ck-body-wrapper">...</div>`) gets automatically removed from DOM when the
* last body collection is {@link ~BodyCollection#detachFromDom detached} and does not require any special handling.
*/
export default class BodyCollection extends ViewCollection {
/**
Expand All @@ -39,10 +70,15 @@ export default class BodyCollection extends ViewCollection {
public readonly locale: Locale;

/**
* The element holding elements of the body region.
* The element holding elements of the body collection.
*/
private _bodyCollectionContainer?: HTMLElement;

/**
* The wrapper element that holds all of the {@link #_bodyCollectionContainer} elements.
*/
private static _bodyWrapper?: HTMLElement;

/**
* Creates a new instance of the {@link module:ui/editorui/bodycollection~BodyCollection}.
*
Expand All @@ -56,7 +92,7 @@ export default class BodyCollection extends ViewCollection {
}

/**
* The element holding elements of the body region.
* The element holding elements of the body collection.
*/
public get bodyCollectionContainer(): HTMLElement | undefined {
return this._bodyCollectionContainer;
Expand All @@ -82,14 +118,12 @@ export default class BodyCollection extends ViewCollection {
children: this
} ).render() as HTMLElement;

let wrapper = document.querySelector( '.ck-body-wrapper' );

if ( !wrapper ) {
wrapper = createElement( document, 'div', { class: 'ck-body-wrapper' } );
document.body.appendChild( wrapper );
if ( !BodyCollection._bodyWrapper ) {
BodyCollection._bodyWrapper = createElement( document, 'div', { class: 'ck-body-wrapper' } );
document.body.appendChild( BodyCollection._bodyWrapper );
}

wrapper.appendChild( this._bodyCollectionContainer );
BodyCollection._bodyWrapper.appendChild( this._bodyCollectionContainer );
}

/**
Expand All @@ -103,10 +137,9 @@ export default class BodyCollection extends ViewCollection {
this._bodyCollectionContainer.remove();
}

const wrapper = document.querySelector( '.ck-body-wrapper' );

if ( wrapper && wrapper.childElementCount == 0 ) {
wrapper.remove();
if ( BodyCollection._bodyWrapper && !BodyCollection._bodyWrapper.childElementCount ) {
BodyCollection._bodyWrapper.remove();
delete BodyCollection._bodyWrapper;
}
}
}
3 changes: 3 additions & 0 deletions packages/ckeditor5-ui/tests/editorui/bodycollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe( 'BodyCollection', () => {
for ( const wrapper of wrappers ) {
wrapper.remove();
}
delete BodyCollection._bodyWrapper;
} );

describe( 'constructor', () => {
Expand Down Expand Up @@ -55,6 +56,7 @@ describe( 'BodyCollection', () => {

expect( wrappers.length ).to.equal( 1 );
expect( wrappers[ 0 ].parentNode ).to.equal( document.body );
expect( BodyCollection._bodyWrapper ).to.equal( wrappers[ 0 ] );

const el = body.bodyCollectionContainer;

Expand Down Expand Up @@ -111,6 +113,7 @@ describe( 'BodyCollection', () => {
expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 1 );
expect( bodyElements.length ).to.equal( 2 );
expect( bodyElements[ 0 ].parentNode ).to.equal( bodyElements[ 1 ].parentNode );
expect( BodyCollection._bodyWrapper ).to.equal( bodyElements[ 0 ].parentNode );
} );

it( 'should render views in proper body collections', () => {
Expand Down

0 comments on commit ce735cc

Please sign in to comment.