@ckeditor/ckeditor5-ui 43.3.1 → 44.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +13 -7
  2. package/dist/badge/badge.d.ts +133 -0
  3. package/dist/dropdown/dropdownview.d.ts +6 -0
  4. package/dist/editorui/bodycollection.d.ts +47 -12
  5. package/dist/editorui/editorui.d.ts +5 -0
  6. package/dist/editorui/evaluationbadge.d.ts +37 -0
  7. package/dist/editorui/poweredby.d.ts +12 -49
  8. package/dist/index-editor.css +52 -3
  9. package/dist/index.css +62 -3
  10. package/dist/index.css.map +1 -1
  11. package/dist/index.js +351 -144
  12. package/dist/index.js.map +1 -1
  13. package/dist/menubar/menubarmenupanelview.d.ts +1 -1
  14. package/dist/menubar/menubarmenuview.d.ts +5 -0
  15. package/dist/menubar/utils.d.ts +1 -0
  16. package/package.json +4 -4
  17. package/src/badge/badge.d.ts +129 -0
  18. package/src/badge/badge.js +218 -0
  19. package/src/dialog/dialogview.js +6 -2
  20. package/src/dropdown/dropdownview.d.ts +6 -0
  21. package/src/dropdown/dropdownview.js +9 -1
  22. package/src/editorui/bodycollection.d.ts +47 -12
  23. package/src/editorui/bodycollection.js +50 -19
  24. package/src/editorui/editorui.d.ts +5 -0
  25. package/src/editorui/editorui.js +3 -0
  26. package/src/editorui/evaluationbadge.d.ts +33 -0
  27. package/src/editorui/evaluationbadge.js +99 -0
  28. package/src/editorui/poweredby.d.ts +12 -49
  29. package/src/editorui/poweredby.js +36 -194
  30. package/src/menubar/menubarmenupanelview.d.ts +1 -1
  31. package/src/menubar/menubarmenuview.d.ts +5 -0
  32. package/src/menubar/menubarmenuview.js +23 -1
  33. package/src/menubar/utils.d.ts +1 -0
  34. package/src/menubar/utils.js +2 -0
  35. package/theme/globals/_evaluationbadge.css +54 -0
  36. package/theme/globals/_poweredby.css +15 -3
  37. package/theme/globals/globals.css +1 -0
@@ -54,4 +54,4 @@ export default class MenuBarMenuPanelView extends View implements FocusableView
54
54
  *
55
55
  * They are reflected as CSS class suffixes on the panel view element.
56
56
  */
57
- export type MenuBarMenuPanelPosition = 'se' | 'sw' | 'ne' | 'nw' | 'w' | 'e';
57
+ export type MenuBarMenuPanelPosition = 'se' | 'sw' | 'ne' | 'nw' | 'w' | 'ws' | 'e' | 'es';
@@ -104,6 +104,11 @@ export default class MenuBarMenuView extends View implements FocusableView {
104
104
  * the {@link module:ui/menubar/menubarview~MenuBarView menu bar} and the UI language direction.
105
105
  */
106
106
  get _panelPositions(): Array<PositioningFunction>;
107
+ /**
108
+ * The default position of the panel when the menu is opened.
109
+ * It is used when the optimal position cannot be calculated.
110
+ */
111
+ private get _defaultMenuPositionName();
107
112
  /**
108
113
  * A function used to calculate the optimal position for the dropdown panel.
109
114
  *
@@ -272,6 +272,7 @@ export declare const MenuBarMenuViewPanelPositioningFunctions: Record<string, Po
272
272
  * groupId: 'insertInline',
273
273
  * items: [
274
274
  * 'menuBar:link',
275
+ * 'menuBar:bookmark',
275
276
  * 'menuBar:comment',
276
277
  * 'menuBar:insertMergeField'
277
278
  * ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-ui",
3
- "version": "43.3.1",
3
+ "version": "44.0.0-alpha.1",
4
4
  "description": "The UI framework and standard UI library of CKEditor 5.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -12,9 +12,9 @@
12
12
  "type": "module",
13
13
  "main": "src/index.js",
14
14
  "dependencies": {
15
- "@ckeditor/ckeditor5-core": "43.3.1",
16
- "@ckeditor/ckeditor5-engine": "43.3.1",
17
- "@ckeditor/ckeditor5-utils": "43.3.1",
15
+ "@ckeditor/ckeditor5-core": "44.0.0-alpha.1",
16
+ "@ckeditor/ckeditor5-engine": "44.0.0-alpha.1",
17
+ "@ckeditor/ckeditor5-utils": "44.0.0-alpha.1",
18
18
  "color-convert": "2.0.1",
19
19
  "color-parse": "1.4.2",
20
20
  "lodash-es": "4.17.21",
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module ui/badge/badge
7
+ */
8
+ import type { Editor } from '@ckeditor/ckeditor5-core';
9
+ import type View from '../view.js';
10
+ declare const Badge_base: {
11
+ new (): import("@ckeditor/ckeditor5-utils").DomEmitter;
12
+ prototype: import("@ckeditor/ckeditor5-utils").DomEmitter;
13
+ };
14
+ /**
15
+ * A helper that enables the badge feature in the editor and renders a custom view next to the bottom of the editable element
16
+ * (editor root, source editing area, etc.) when the editor is focused.
17
+ *
18
+ * @private
19
+ */
20
+ export default abstract class Badge extends /* #__PURE__ */ Badge_base {
21
+ /**
22
+ * Editor instance the helper was created for.
23
+ */
24
+ protected readonly editor: Editor;
25
+ /**
26
+ * A reference to the balloon panel hosting and positioning the badge content.
27
+ */
28
+ private _balloonView;
29
+ /**
30
+ * A throttled version of the {@link #_showBalloon} method meant for frequent use to avoid performance loss.
31
+ */
32
+ private _showBalloonThrottled;
33
+ /**
34
+ * A reference to the last editable element (root, source editing area, etc.) focused by the user.
35
+ * Since the focus can move to other focusable elements in the UI, this reference allows positioning the balloon over the
36
+ * right element whether the user is typing or using the UI.
37
+ */
38
+ private _lastFocusedEditableElement;
39
+ /**
40
+ * An additional CSS class added to the `BalloonView`.
41
+ */
42
+ private readonly _balloonClass;
43
+ /**
44
+ * Creates a badge for a given editor. The feature is initialized on Editor#ready
45
+ * event.
46
+ */
47
+ protected constructor(editor: Editor, options?: {
48
+ balloonClass?: string;
49
+ });
50
+ /**
51
+ * Destroys the badge along with its view.
52
+ */
53
+ destroy(): void;
54
+ /**
55
+ * Enables badge label once the editor (ui) is ready.
56
+ */
57
+ protected _handleEditorReady(): void;
58
+ /**
59
+ * Returns normalized configuration for the badge.
60
+ */
61
+ protected _getNormalizedConfig(): BadgeConfig;
62
+ /**
63
+ * Creates the badge content.
64
+ */
65
+ protected abstract _createBadgeContent(): View<HTMLElement>;
66
+ /**
67
+ * Enables the badge feature.
68
+ */
69
+ protected abstract _isEnabled(): boolean;
70
+ /**
71
+ * Attempts to display the balloon with the badge view.
72
+ */
73
+ private _showBalloon;
74
+ /**
75
+ * Hides the badge balloon if already visible.
76
+ */
77
+ private _hideBalloon;
78
+ /**
79
+ * Creates an instance of the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon panel}
80
+ * with the badge view inside ready for positioning.
81
+ */
82
+ private _createBalloonView;
83
+ /**
84
+ * Returns the options for attaching the balloon to the focused editable element.
85
+ */
86
+ private _getBalloonAttachOptions;
87
+ /**
88
+ * Updates the {@link #_lastFocusedEditableElement} based on the state of the global focus tracker.
89
+ */
90
+ private _updateLastFocusedEditableElement;
91
+ }
92
+ /**
93
+ * The badge configuration options.
94
+ **/
95
+ export interface BadgeConfig {
96
+ /**
97
+ * The position of the badge.
98
+ *
99
+ * * When `'inside'`, the badge will be displayed within the boundaries of the editing area.
100
+ * * When `'border'`, the basge will be displayed over the bottom border of the editing area.
101
+ *
102
+ * @default 'border'
103
+ */
104
+ position: 'inside' | 'border';
105
+ /**
106
+ * Allows choosing the side of the editing area where the badge will be displayed.
107
+ *
108
+ * **Note:** If {@link module:core/editor/editorconfig~EditorConfig#language `config.language`} is set to an RTL (right-to-left)
109
+ * language, the side switches to `'left'` by default.
110
+ *
111
+ * @default 'right'
112
+ */
113
+ side: 'left' | 'right';
114
+ /**
115
+ * The vertical distance the badge can be moved away from its default position.
116
+ *
117
+ * **Note:** If `position` is `'border'`, the offset is measured from the (vertical) center of the badge.
118
+ *
119
+ * @default 5
120
+ */
121
+ verticalOffset: number;
122
+ /**
123
+ * The horizontal distance between the side of the editing root and the nearest side of the badge.
124
+ *
125
+ * @default 5
126
+ */
127
+ horizontalOffset: number;
128
+ }
129
+ export {};
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ import { Rect, DomEmitterMixin } from '@ckeditor/ckeditor5-utils';
6
+ import BalloonPanelView from '../panel/balloon/balloonpanelview.js';
7
+ import { throttle } from 'lodash-es';
8
+ // ⚠ Note, whenever changing the threshold, make sure to update the docs/support/managing-ckeditor-logo.md docs
9
+ // as this information is also mentioned there ⚠.
10
+ const NARROW_ROOT_HEIGHT_THRESHOLD = 50;
11
+ const NARROW_ROOT_WIDTH_THRESHOLD = 350;
12
+ /**
13
+ * A helper that enables the badge feature in the editor and renders a custom view next to the bottom of the editable element
14
+ * (editor root, source editing area, etc.) when the editor is focused.
15
+ *
16
+ * @private
17
+ */
18
+ export default class Badge extends /* #__PURE__ */ DomEmitterMixin() {
19
+ /**
20
+ * Creates a badge for a given editor. The feature is initialized on Editor#ready
21
+ * event.
22
+ */
23
+ constructor(editor, options = {}) {
24
+ super();
25
+ /**
26
+ * A reference to the balloon panel hosting and positioning the badge content.
27
+ */
28
+ this._balloonView = null;
29
+ /**
30
+ * A throttled version of the {@link #_showBalloon} method meant for frequent use to avoid performance loss.
31
+ */
32
+ this._showBalloonThrottled = throttle(() => this._showBalloon(), 50, { leading: true });
33
+ /**
34
+ * A reference to the last editable element (root, source editing area, etc.) focused by the user.
35
+ * Since the focus can move to other focusable elements in the UI, this reference allows positioning the balloon over the
36
+ * right element whether the user is typing or using the UI.
37
+ */
38
+ this._lastFocusedEditableElement = null;
39
+ this.editor = editor;
40
+ this._balloonClass = options.balloonClass;
41
+ editor.on('ready', () => this._handleEditorReady());
42
+ }
43
+ /**
44
+ * Destroys the badge along with its view.
45
+ */
46
+ destroy() {
47
+ const balloon = this._balloonView;
48
+ if (balloon) {
49
+ // Balloon gets destroyed by the body collection.
50
+ // The badge view gets destroyed by the balloon.
51
+ balloon.unpin();
52
+ this._balloonView = null;
53
+ }
54
+ this._showBalloonThrottled.cancel();
55
+ this.stopListening();
56
+ }
57
+ /**
58
+ * Enables badge label once the editor (ui) is ready.
59
+ */
60
+ _handleEditorReady() {
61
+ const editor = this.editor;
62
+ if (!this._isEnabled()) {
63
+ return;
64
+ }
65
+ // No view means no body collection to append the badge balloon to.
66
+ if (!editor.ui.view) {
67
+ return;
68
+ }
69
+ editor.ui.focusTracker.on('change:isFocused', (evt, data, isFocused) => {
70
+ this._updateLastFocusedEditableElement();
71
+ if (isFocused) {
72
+ this._showBalloon();
73
+ }
74
+ else {
75
+ this._hideBalloon();
76
+ }
77
+ });
78
+ editor.ui.focusTracker.on('change:focusedElement', (evt, data, focusedElement) => {
79
+ this._updateLastFocusedEditableElement();
80
+ if (focusedElement) {
81
+ this._showBalloon();
82
+ }
83
+ });
84
+ editor.ui.on('update', () => {
85
+ this._showBalloonThrottled();
86
+ });
87
+ }
88
+ /**
89
+ * Returns normalized configuration for the badge.
90
+ */
91
+ _getNormalizedConfig() {
92
+ return {
93
+ side: this.editor.locale.contentLanguageDirection === 'ltr' ? 'right' : 'left',
94
+ position: 'border',
95
+ verticalOffset: 0,
96
+ horizontalOffset: 5
97
+ };
98
+ }
99
+ /**
100
+ * Attempts to display the balloon with the badge view.
101
+ */
102
+ _showBalloon() {
103
+ const attachOptions = this._getBalloonAttachOptions();
104
+ if (!attachOptions) {
105
+ return;
106
+ }
107
+ if (!this._balloonView) {
108
+ this._balloonView = this._createBalloonView();
109
+ }
110
+ this._balloonView.pin(attachOptions);
111
+ }
112
+ /**
113
+ * Hides the badge balloon if already visible.
114
+ */
115
+ _hideBalloon() {
116
+ if (this._balloonView) {
117
+ this._balloonView.unpin();
118
+ }
119
+ }
120
+ /**
121
+ * Creates an instance of the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon panel}
122
+ * with the badge view inside ready for positioning.
123
+ */
124
+ _createBalloonView() {
125
+ const editor = this.editor;
126
+ const balloon = new BalloonPanelView();
127
+ const view = this._createBadgeContent();
128
+ balloon.content.add(view);
129
+ if (this._balloonClass) {
130
+ balloon.class = this._balloonClass;
131
+ }
132
+ editor.ui.view.body.add(balloon);
133
+ return balloon;
134
+ }
135
+ /**
136
+ * Returns the options for attaching the balloon to the focused editable element.
137
+ */
138
+ _getBalloonAttachOptions() {
139
+ if (!this._lastFocusedEditableElement) {
140
+ return null;
141
+ }
142
+ const badgeConfig = this._getNormalizedConfig();
143
+ const positioningFunction = badgeConfig.side === 'right' ?
144
+ getLowerRightCornerPosition(this._lastFocusedEditableElement, badgeConfig) :
145
+ getLowerLeftCornerPosition(this._lastFocusedEditableElement, badgeConfig);
146
+ return {
147
+ target: this._lastFocusedEditableElement,
148
+ positions: [positioningFunction]
149
+ };
150
+ }
151
+ /**
152
+ * Updates the {@link #_lastFocusedEditableElement} based on the state of the global focus tracker.
153
+ */
154
+ _updateLastFocusedEditableElement() {
155
+ const editor = this.editor;
156
+ const isFocused = editor.ui.focusTracker.isFocused;
157
+ const focusedElement = editor.ui.focusTracker.focusedElement;
158
+ if (!isFocused || !focusedElement) {
159
+ this._lastFocusedEditableElement = null;
160
+ return;
161
+ }
162
+ const editableEditorElements = Array.from(editor.ui.getEditableElementsNames()).map(name => {
163
+ return editor.ui.getEditableElement(name);
164
+ });
165
+ if (editableEditorElements.includes(focusedElement)) {
166
+ this._lastFocusedEditableElement = focusedElement;
167
+ }
168
+ else {
169
+ // If it's none of the editable element, then the focus is somewhere in the UI. Let's display the badge
170
+ // over the first element then.
171
+ this._lastFocusedEditableElement = editableEditorElements[0];
172
+ }
173
+ }
174
+ }
175
+ function getLowerRightCornerPosition(focusedEditableElement, config) {
176
+ return getLowerCornerPosition(focusedEditableElement, config, (rootRect, balloonRect) => {
177
+ return rootRect.left + rootRect.width - balloonRect.width - config.horizontalOffset;
178
+ });
179
+ }
180
+ function getLowerLeftCornerPosition(focusedEditableElement, config) {
181
+ return getLowerCornerPosition(focusedEditableElement, config, rootRect => rootRect.left + config.horizontalOffset);
182
+ }
183
+ function getLowerCornerPosition(focusedEditableElement, config, getBalloonLeft) {
184
+ return (visibleEditableElementRect, balloonRect) => {
185
+ const editableElementRect = new Rect(focusedEditableElement);
186
+ if (editableElementRect.width < NARROW_ROOT_WIDTH_THRESHOLD || editableElementRect.height < NARROW_ROOT_HEIGHT_THRESHOLD) {
187
+ return null;
188
+ }
189
+ let balloonTop;
190
+ if (config.position === 'inside') {
191
+ balloonTop = editableElementRect.bottom - balloonRect.height;
192
+ }
193
+ else {
194
+ balloonTop = editableElementRect.bottom - balloonRect.height / 2;
195
+ }
196
+ balloonTop -= config.verticalOffset;
197
+ const balloonLeft = getBalloonLeft(editableElementRect, balloonRect);
198
+ // Clone the editable element rect and place it where the balloon would be placed.
199
+ // This will allow getVisible() to work from editable element's perspective (rect source).
200
+ // and yield a result as if the balloon was on the same (scrollable) layer as the editable element.
201
+ const newBalloonPositionRect = visibleEditableElementRect
202
+ .clone()
203
+ .moveTo(balloonLeft, balloonTop)
204
+ .getIntersection(balloonRect.clone().moveTo(balloonLeft, balloonTop));
205
+ const newBalloonPositionVisibleRect = newBalloonPositionRect.getVisible();
206
+ if (!newBalloonPositionVisibleRect || newBalloonPositionVisibleRect.getArea() < balloonRect.getArea()) {
207
+ return null;
208
+ }
209
+ return {
210
+ top: balloonTop,
211
+ left: balloonLeft,
212
+ name: `position_${config.position}-side_${config.side}`,
213
+ config: {
214
+ withArrow: false
215
+ }
216
+ };
217
+ };
218
+ }
@@ -126,8 +126,12 @@ class DialogView extends /* #__PURE__ */ DraggableViewMixin(View) {
126
126
  render() {
127
127
  super.render();
128
128
  this.keystrokes.set('Esc', (data, cancel) => {
129
- this.fire('close', { source: 'escKeyPress' });
130
- cancel();
129
+ // Do not react to the Esc key if the event has already been handled and defaultPrevented
130
+ // by some logic of the dialog guest (child) view (https://github.com/ckeditor/ckeditor5/issues/17343).
131
+ if (!data.defaultPrevented) {
132
+ this.fire('close', { source: 'escKeyPress' });
133
+ cancel();
134
+ }
131
135
  });
132
136
  // Support for dragging the modal.
133
137
  this.on('drag', (evt, { deltaX, deltaY }) => {
@@ -189,6 +189,12 @@ export default class DropdownView extends View<HTMLDivElement> {
189
189
  * utility considering the direction of the language the UI of the editor is displayed in.
190
190
  */
191
191
  private get _panelPositions();
192
+ /**
193
+ * Returns the default position of the dropdown panel based on the direction of the UI language.
194
+ * It is used when the {@link #panelPosition} is set to `'auto'` and the panel has not found a
195
+ * suitable position to fit into the viewport.
196
+ */
197
+ private get _defaultPanelPositionName();
192
198
  /**
193
199
  * A set of positioning functions used by the dropdown view to determine
194
200
  * the optimal position (i.e. fitting into the browser viewport) of its
@@ -134,7 +134,7 @@ class DropdownView extends View {
134
134
  fitInViewport: true,
135
135
  positions: this._panelPositions
136
136
  });
137
- this.panelView.position = (optimalPanelPosition ? optimalPanelPosition.name : this._panelPositions[0].name);
137
+ this.panelView.position = (optimalPanelPosition ? optimalPanelPosition.name : this._defaultPanelPositionName);
138
138
  }
139
139
  else {
140
140
  this.panelView.position = this.panelPosition;
@@ -192,6 +192,14 @@ class DropdownView extends View {
192
192
  ];
193
193
  }
194
194
  }
195
+ /**
196
+ * Returns the default position of the dropdown panel based on the direction of the UI language.
197
+ * It is used when the {@link #panelPosition} is set to `'auto'` and the panel has not found a
198
+ * suitable position to fit into the viewport.
199
+ */
200
+ get _defaultPanelPositionName() {
201
+ return this.locale.uiLanguageDirection === 'rtl' ? 'sw' : 'se';
202
+ }
195
203
  }
196
204
  /**
197
205
  * A set of positioning functions used by the dropdown view to determine
@@ -6,20 +6,51 @@ import ViewCollection from '../viewcollection.js';
6
6
  import type View from '../view.js';
7
7
  import { type Locale } from '@ckeditor/ckeditor5-utils';
8
8
  /**
9
- * This is a special {@link module:ui/viewcollection~ViewCollection} dedicated to elements that are detached
10
- * from the DOM structure of the editor, like panels, icons, etc.
9
+ * This is a special {@link module:ui/viewcollection~ViewCollection} dedicated to elements that are detached from the DOM structure of
10
+ * the editor, like floating panels, floating toolbars, dialogs, etc.
11
11
  *
12
- * The body collection is available in the {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} property.
12
+ * The body collection is available under the {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} property.
13
13
  * Any plugin can add a {@link module:ui/view~View view} to this collection.
14
- * These views will render in a container placed directly in the `<body>` element.
15
- * The editor will detach and destroy this collection when the editor will be {@link module:core/editor/editor~Editor#destroy destroyed}.
16
14
  *
17
- * If you need to control the life cycle of the body collection on your own, you can create your own instance of this class.
15
+ * All views added to a body collection render in a dedicated DOM container (`<div class="ck ck-body ...">...</div>`). All body collection
16
+ * containers render in a common shared (`<div class="ck-body-wrapper">...</div>`) in the DOM to limit the pollution of
17
+ * the `<body>` element. The resulting DOM structure is as follows:
18
18
  *
19
- * A body collection will render itself automatically in the DOM body element as soon as you call {@link ~BodyCollection#attachToDom}.
20
- * If you create multiple body collections, this class will create a special wrapper element in the DOM to limit the number of
21
- * elements created directly in the body and remove it when the last body collection will be
22
- * {@link ~BodyCollection#detachFromDom detached}.
19
+ * ```html
20
+ * <body>
21
+ * <!-- Content of the webpage... -->
22
+ *
23
+ * <!-- The shared wrapper for all body collection containers. -->
24
+ * <div class="ck-body-wrapper">
25
+ * <!-- The container of the first body collection instance. -->
26
+ * <div class="ck ck-body ...">
27
+ * <!-- View elements belonging to the first body collection -->
28
+ * </div>
29
+ *
30
+ * <!-- The container of the second body collection instance. -->
31
+ * <div class="ck ck-body ...">...</div>
32
+ *
33
+ * <!-- More body collection containers for the rest of instances... -->
34
+ * </div>
35
+ * </body>
36
+ * ```
37
+ *
38
+ * By default, the {@link module:ui/editorui/editoruiview~EditorUIView `editor.ui.view`} manages the life cycle of the
39
+ * {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} collection, attaching and detaching it
40
+ * when the editor gets created or {@link module:core/editor/editor~Editor#destroy destroyed}.
41
+ *
42
+ * # Custom body collection instances
43
+ *
44
+ * Even though most editor instances come with a built-in body collection
45
+ * ({@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`}), you can create your own instance of this
46
+ * class if you need to control their life cycle.
47
+ *
48
+ * The life cycle of a custom body collection must be handled manually by the developer using the dedicated API:
49
+ * * A body collection will render itself automatically in the DOM as soon as you call {@link ~BodyCollection#attachToDom}.
50
+ * * Calling {@link ~BodyCollection#detachFromDom} will remove the collection from the DOM.
51
+ *
52
+ * **Note**: The shared collection wrapper (`<div class="ck-body-wrapper">...</div>`) gets automatically removed from DOM when the
53
+ * last body collection is {@link ~BodyCollection#detachFromDom detached} and does not require any special handling.
23
54
  */
24
55
  export default class BodyCollection extends ViewCollection {
25
56
  /**
@@ -28,9 +59,13 @@ export default class BodyCollection extends ViewCollection {
28
59
  */
29
60
  readonly locale: Locale;
30
61
  /**
31
- * The element holding elements of the body region.
62
+ * The element holding elements of the body collection.
32
63
  */
33
64
  private _bodyCollectionContainer?;
65
+ /**
66
+ * The wrapper element that holds all of the {@link #_bodyCollectionContainer} elements.
67
+ */
68
+ private static _bodyWrapper?;
34
69
  /**
35
70
  * Creates a new instance of the {@link module:ui/editorui/bodycollection~BodyCollection}.
36
71
  *
@@ -39,7 +74,7 @@ export default class BodyCollection extends ViewCollection {
39
74
  */
40
75
  constructor(locale: Locale, initialItems?: Iterable<View>);
41
76
  /**
42
- * The element holding elements of the body region.
77
+ * The element holding elements of the body collection.
43
78
  */
44
79
  get bodyCollectionContainer(): HTMLElement | undefined;
45
80
  /**
@@ -10,20 +10,51 @@ import Template from '../template.js';
10
10
  import ViewCollection from '../viewcollection.js';
11
11
  import { createElement } from '@ckeditor/ckeditor5-utils';
12
12
  /**
13
- * This is a special {@link module:ui/viewcollection~ViewCollection} dedicated to elements that are detached
14
- * from the DOM structure of the editor, like panels, icons, etc.
13
+ * This is a special {@link module:ui/viewcollection~ViewCollection} dedicated to elements that are detached from the DOM structure of
14
+ * the editor, like floating panels, floating toolbars, dialogs, etc.
15
15
  *
16
- * The body collection is available in the {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} property.
16
+ * The body collection is available under the {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} property.
17
17
  * Any plugin can add a {@link module:ui/view~View view} to this collection.
18
- * These views will render in a container placed directly in the `<body>` element.
19
- * The editor will detach and destroy this collection when the editor will be {@link module:core/editor/editor~Editor#destroy destroyed}.
20
18
  *
21
- * If you need to control the life cycle of the body collection on your own, you can create your own instance of this class.
19
+ * All views added to a body collection render in a dedicated DOM container (`<div class="ck ck-body ...">...</div>`). All body collection
20
+ * containers render in a common shared (`<div class="ck-body-wrapper">...</div>`) in the DOM to limit the pollution of
21
+ * the `<body>` element. The resulting DOM structure is as follows:
22
22
  *
23
- * A body collection will render itself automatically in the DOM body element as soon as you call {@link ~BodyCollection#attachToDom}.
24
- * If you create multiple body collections, this class will create a special wrapper element in the DOM to limit the number of
25
- * elements created directly in the body and remove it when the last body collection will be
26
- * {@link ~BodyCollection#detachFromDom detached}.
23
+ * ```html
24
+ * <body>
25
+ * <!-- Content of the webpage... -->
26
+ *
27
+ * <!-- The shared wrapper for all body collection containers. -->
28
+ * <div class="ck-body-wrapper">
29
+ * <!-- The container of the first body collection instance. -->
30
+ * <div class="ck ck-body ...">
31
+ * <!-- View elements belonging to the first body collection -->
32
+ * </div>
33
+ *
34
+ * <!-- The container of the second body collection instance. -->
35
+ * <div class="ck ck-body ...">...</div>
36
+ *
37
+ * <!-- More body collection containers for the rest of instances... -->
38
+ * </div>
39
+ * </body>
40
+ * ```
41
+ *
42
+ * By default, the {@link module:ui/editorui/editoruiview~EditorUIView `editor.ui.view`} manages the life cycle of the
43
+ * {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} collection, attaching and detaching it
44
+ * when the editor gets created or {@link module:core/editor/editor~Editor#destroy destroyed}.
45
+ *
46
+ * # Custom body collection instances
47
+ *
48
+ * Even though most editor instances come with a built-in body collection
49
+ * ({@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`}), you can create your own instance of this
50
+ * class if you need to control their life cycle.
51
+ *
52
+ * The life cycle of a custom body collection must be handled manually by the developer using the dedicated API:
53
+ * * A body collection will render itself automatically in the DOM as soon as you call {@link ~BodyCollection#attachToDom}.
54
+ * * Calling {@link ~BodyCollection#detachFromDom} will remove the collection from the DOM.
55
+ *
56
+ * **Note**: The shared collection wrapper (`<div class="ck-body-wrapper">...</div>`) gets automatically removed from DOM when the
57
+ * last body collection is {@link ~BodyCollection#detachFromDom detached} and does not require any special handling.
27
58
  */
28
59
  export default class BodyCollection extends ViewCollection {
29
60
  /**
@@ -37,7 +68,7 @@ export default class BodyCollection extends ViewCollection {
37
68
  this.locale = locale;
38
69
  }
39
70
  /**
40
- * The element holding elements of the body region.
71
+ * The element holding elements of the body collection.
41
72
  */
42
73
  get bodyCollectionContainer() {
43
74
  return this._bodyCollectionContainer;
@@ -61,12 +92,12 @@ export default class BodyCollection extends ViewCollection {
61
92
  },
62
93
  children: this
63
94
  }).render();
64
- let wrapper = document.querySelector('.ck-body-wrapper');
65
- if (!wrapper) {
66
- wrapper = createElement(document, 'div', { class: 'ck-body-wrapper' });
67
- document.body.appendChild(wrapper);
95
+ // Create a shared wrapper if there were none or the previous one got disconnected from DOM.
96
+ if (!BodyCollection._bodyWrapper || !BodyCollection._bodyWrapper.isConnected) {
97
+ BodyCollection._bodyWrapper = createElement(document, 'div', { class: 'ck-body-wrapper' });
98
+ document.body.appendChild(BodyCollection._bodyWrapper);
68
99
  }
69
- wrapper.appendChild(this._bodyCollectionContainer);
100
+ BodyCollection._bodyWrapper.appendChild(this._bodyCollectionContainer);
70
101
  }
71
102
  /**
72
103
  * Detaches the collection from the DOM structure. Use this method when you do not need to use the body collection
@@ -77,9 +108,9 @@ export default class BodyCollection extends ViewCollection {
77
108
  if (this._bodyCollectionContainer) {
78
109
  this._bodyCollectionContainer.remove();
79
110
  }
80
- const wrapper = document.querySelector('.ck-body-wrapper');
81
- if (wrapper && wrapper.childElementCount == 0) {
82
- wrapper.remove();
111
+ if (BodyCollection._bodyWrapper && !BodyCollection._bodyWrapper.childElementCount) {
112
+ BodyCollection._bodyWrapper.remove();
113
+ delete BodyCollection._bodyWrapper;
83
114
  }
84
115
  }
85
116
  }
@@ -8,6 +8,7 @@
8
8
  import ComponentFactory from '../componentfactory.js';
9
9
  import TooltipManager from '../tooltipmanager.js';
10
10
  import PoweredBy from './poweredby.js';
11
+ import EvaluationBadge from './evaluationbadge.js';
11
12
  import AriaLiveAnnouncer from '../arialiveannouncer.js';
12
13
  import type EditorUIView from './editoruiview.js';
13
14
  import type ToolbarView from '../toolbar/toolbarview.js';
@@ -44,6 +45,10 @@ export default abstract class EditorUI extends /* #__PURE__ */ EditorUI_base {
44
45
  * A helper that enables the "powered by" feature in the editor and renders a link to the project's webpage.
45
46
  */
46
47
  readonly poweredBy: PoweredBy;
48
+ /**
49
+ * A helper that enables the "evaluation badge" feature in the editor.
50
+ */
51
+ readonly evaluationBadge: EvaluationBadge;
47
52
  /**
48
53
  * A helper that manages the content of an `aria-live` regions used by editor features to announce status changes
49
54
  * to screen readers.