@ckeditor/ckeditor5-ui 45.0.0 → 45.1.0-alpha.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-ui",
3
- "version": "45.0.0",
3
+ "version": "45.1.0-alpha.0",
4
4
  "description": "The UI framework and standard UI library of CKEditor 5.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -17,11 +17,11 @@
17
17
  "type": "module",
18
18
  "main": "src/index.js",
19
19
  "dependencies": {
20
- "@ckeditor/ckeditor5-core": "45.0.0",
21
- "@ckeditor/ckeditor5-editor-multi-root": "45.0.0",
22
- "@ckeditor/ckeditor5-engine": "45.0.0",
23
- "@ckeditor/ckeditor5-icons": "45.0.0",
24
- "@ckeditor/ckeditor5-utils": "45.0.0",
20
+ "@ckeditor/ckeditor5-core": "45.1.0-alpha.0",
21
+ "@ckeditor/ckeditor5-editor-multi-root": "45.1.0-alpha.0",
22
+ "@ckeditor/ckeditor5-engine": "45.1.0-alpha.0",
23
+ "@ckeditor/ckeditor5-icons": "45.1.0-alpha.0",
24
+ "@ckeditor/ckeditor5-utils": "45.1.0-alpha.0",
25
25
  "@types/color-convert": "2.0.4",
26
26
  "color-convert": "2.0.1",
27
27
  "color-parse": "1.4.2",
@@ -114,14 +114,15 @@ export default class ColorGridsFragmentView extends View {
114
114
  * Creates an instance of the view.
115
115
  *
116
116
  * @param locale The localization services instance.
117
- * @param colors An array with definitions of colors to be displayed in the table.
118
- * @param columns The number of columns in the color grid.
119
- * @param removeButtonLabel The label of the button responsible for removing the color.
120
- * @param colorPickerLabel The label of the button responsible for color picker appearing.
121
- * @param documentColorsLabel The label for the section with the document colors.
122
- * @param documentColorsCount The number of colors in the document colors section inside the color dropdown.
123
- * @param focusTracker Tracks information about the DOM focus in the list.
124
- * @param focusables A collection of views that can be focused in the view.
117
+ * @param options Constructor options.
118
+ * @param options.colors An array with definitions of colors to be displayed in the table.
119
+ * @param options.columns The number of columns in the color grid.
120
+ * @param options.removeButtonLabel The label of the button responsible for removing the color.
121
+ * @param options.colorPickerLabel The label of the button responsible for color picker appearing.
122
+ * @param options.documentColorsLabel The label for the section with the document colors.
123
+ * @param options.documentColorsCount The number of colors in the document colors section inside the color dropdown.
124
+ * @param options.focusTracker Tracks information about the DOM focus in the list.
125
+ * @param options.focusables A collection of views that can be focused in the view.
125
126
  */
126
127
  constructor(locale: Locale, { colors, columns, removeButtonLabel, documentColorsLabel, documentColorsCount, colorPickerLabel, focusTracker, focusables }: {
127
128
  colors: Array<ColorDefinition>;
@@ -105,14 +105,15 @@ export default class ColorGridsFragmentView extends View {
105
105
  * Creates an instance of the view.
106
106
  *
107
107
  * @param locale The localization services instance.
108
- * @param colors An array with definitions of colors to be displayed in the table.
109
- * @param columns The number of columns in the color grid.
110
- * @param removeButtonLabel The label of the button responsible for removing the color.
111
- * @param colorPickerLabel The label of the button responsible for color picker appearing.
112
- * @param documentColorsLabel The label for the section with the document colors.
113
- * @param documentColorsCount The number of colors in the document colors section inside the color dropdown.
114
- * @param focusTracker Tracks information about the DOM focus in the list.
115
- * @param focusables A collection of views that can be focused in the view.
108
+ * @param options Constructor options.
109
+ * @param options.colors An array with definitions of colors to be displayed in the table.
110
+ * @param options.columns The number of columns in the color grid.
111
+ * @param options.removeButtonLabel The label of the button responsible for removing the color.
112
+ * @param options.colorPickerLabel The label of the button responsible for color picker appearing.
113
+ * @param options.documentColorsLabel The label for the section with the document colors.
114
+ * @param options.documentColorsCount The number of colors in the document colors section inside the color dropdown.
115
+ * @param options.focusTracker Tracks information about the DOM focus in the list.
116
+ * @param options.focusables A collection of views that can be focused in the view.
116
117
  */
117
118
  constructor(locale, { colors, columns, removeButtonLabel, documentColorsLabel, documentColorsCount, colorPickerLabel, focusTracker, focusables }) {
118
119
  super(locale);
@@ -77,10 +77,11 @@ export default class ColorPickerFragmentView extends View {
77
77
  * Creates an instance of the view.
78
78
  *
79
79
  * @param locale The localization services instance.
80
- * @param focusTracker Tracks information about the DOM focus in the list.
81
- * @param focusables A collection of views that can be focused in the view..
82
- * @param keystrokes An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
83
- * @param colorPickerViewConfig The configuration of color picker feature. If set to `false`, the color picker
80
+ * @param options Constructor options.
81
+ * @param options.focusTracker Tracks information about the DOM focus in the list.
82
+ * @param options.focusables A collection of views that can be focused in the view.
83
+ * @param options.keystrokes An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
84
+ * @param options.colorPickerViewConfig The configuration of color picker feature. If set to `false`, the color picker
84
85
  * will not be rendered.
85
86
  */
86
87
  constructor(locale: Locale, { focusTracker, focusables, keystrokes, colorPickerViewConfig }: {
@@ -66,10 +66,11 @@ export default class ColorPickerFragmentView extends View {
66
66
  * Creates an instance of the view.
67
67
  *
68
68
  * @param locale The localization services instance.
69
- * @param focusTracker Tracks information about the DOM focus in the list.
70
- * @param focusables A collection of views that can be focused in the view..
71
- * @param keystrokes An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
72
- * @param colorPickerViewConfig The configuration of color picker feature. If set to `false`, the color picker
69
+ * @param options Constructor options.
70
+ * @param options.focusTracker Tracks information about the DOM focus in the list.
71
+ * @param options.focusables A collection of views that can be focused in the view.
72
+ * @param options.keystrokes An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
73
+ * @param options.colorPickerViewConfig The configuration of color picker feature. If set to `false`, the color picker
73
74
  * will not be rendered.
74
75
  */
75
76
  constructor(locale, { focusTracker, focusables, keystrokes, colorPickerViewConfig }) {
@@ -120,13 +120,14 @@ export default class ColorSelectorView extends View {
120
120
  * Creates a view to be inserted as a child of {@link module:ui/dropdown/dropdownview~DropdownView}.
121
121
  *
122
122
  * @param locale The localization services instance.
123
- * @param colors An array with definitions of colors to be displayed in the table.
124
- * @param columns The number of columns in the color grid.
125
- * @param removeButtonLabel The label of the button responsible for removing the color.
126
- * @param colorPickerLabel The label of the button responsible for color picker appearing.
127
- * @param documentColorsLabel The label for the section with the document colors.
128
- * @param documentColorsCount The number of colors in the document colors section inside the color dropdown.
129
- * @param colorPickerViewConfig The configuration of color picker feature. If set to `false`, the color picker will be hidden.
123
+ * @param options Constructor options.
124
+ * @param options.colors An array with definitions of colors to be displayed in the table.
125
+ * @param options.columns The number of columns in the color grid.
126
+ * @param options.removeButtonLabel The label of the button responsible for removing the color.
127
+ * @param options.colorPickerLabel The label of the button responsible for color picker appearing.
128
+ * @param options.documentColorsLabel The label for the section with the document colors.
129
+ * @param options.documentColorsCount The number of colors in the document colors section inside the color dropdown.
130
+ * @param options.colorPickerViewConfig The configuration of color picker feature. If set to `false`, the color picker will be hidden.
130
131
  */
131
132
  constructor(locale: Locale, { colors, columns, removeButtonLabel, documentColorsLabel, documentColorsCount, colorPickerLabel, colorPickerViewConfig }: {
132
133
  colors: Array<ColorDefinition>;
@@ -101,13 +101,14 @@ export default class ColorSelectorView extends View {
101
101
  * Creates a view to be inserted as a child of {@link module:ui/dropdown/dropdownview~DropdownView}.
102
102
  *
103
103
  * @param locale The localization services instance.
104
- * @param colors An array with definitions of colors to be displayed in the table.
105
- * @param columns The number of columns in the color grid.
106
- * @param removeButtonLabel The label of the button responsible for removing the color.
107
- * @param colorPickerLabel The label of the button responsible for color picker appearing.
108
- * @param documentColorsLabel The label for the section with the document colors.
109
- * @param documentColorsCount The number of colors in the document colors section inside the color dropdown.
110
- * @param colorPickerViewConfig The configuration of color picker feature. If set to `false`, the color picker will be hidden.
104
+ * @param options Constructor options.
105
+ * @param options.colors An array with definitions of colors to be displayed in the table.
106
+ * @param options.columns The number of columns in the color grid.
107
+ * @param options.removeButtonLabel The label of the button responsible for removing the color.
108
+ * @param options.colorPickerLabel The label of the button responsible for color picker appearing.
109
+ * @param options.documentColorsLabel The label for the section with the document colors.
110
+ * @param options.documentColorsCount The number of colors in the document colors section inside the color dropdown.
111
+ * @param options.colorPickerViewConfig The configuration of color picker feature. If set to `false`, the color picker will be hidden.
111
112
  */
112
113
  constructor(locale, { colors, columns, removeButtonLabel, documentColorsLabel, documentColorsCount, colorPickerLabel, colorPickerViewConfig }) {
113
114
  super(locale);
@@ -145,7 +146,7 @@ export default class ColorSelectorView extends View {
145
146
  this.colorPickerFragmentView.bind('isVisible').to(this, '_isColorPickerFragmentVisible');
146
147
  /**
147
148
  * This is kind of bindings. Unfortunately we could not use this.bind() method because the same property
148
- * can not be bound twice. So this is work around how to bind 'selectedColor' property between components.
149
+ * cannot be bound twice. So this is work around how to bind 'selectedColor' property between components.
149
150
  */
150
151
  this.on('change:selectedColor', (evt, evtName, data) => {
151
152
  this.colorGridsFragmentView.set('selectedColor', data);
@@ -85,7 +85,7 @@ export default class ComponentFactory {
85
85
  * {@link module:ui/componentfactory~ComponentFactory#add added} to the factory.
86
86
  *
87
87
  * @error componentfactory-item-missing
88
- * @param name The name of the missing component.
88
+ * @param {string} name The name of the missing component.
89
89
  */
90
90
  throw new CKEditorError('componentfactory-item-missing', this, { name });
91
91
  }
@@ -79,8 +79,8 @@ export default class DropdownPanelView extends View {
79
79
  * provides the `focus()` method for the best user experience.
80
80
  *
81
81
  * @error ui-dropdown-panel-focus-child-missing-focus
82
- * @param childView
83
- * @param dropdownPanel
82
+ * @param {module:ui/view~View} childView Child view.
83
+ * @param {module:ui/dropdown/dropdownpanelview~DropdownPanelView} dropdownPanel A parent of a child.
84
84
  */
85
85
  logWarning('ui-dropdown-panel-focus-child-missing-focus', { childView: this.children.first, dropdownPanel: this });
86
86
  }
@@ -79,8 +79,6 @@ import type { DropdownMenuDefinition } from './menu/utils.js';
79
79
  *
80
80
  * @param locale The locale instance.
81
81
  * @param ButtonClassOrInstance The dropdown button view class. Needs to implement the
82
- * @param behaviorOptions Attributes for the default behavior of the dropdown.
83
- *
84
82
  * {@link module:ui/dropdown/button/dropdownbutton~DropdownButton} interface.
85
83
  * @returns The dropdown view instance.
86
84
  */
@@ -87,8 +87,6 @@ import ListItemButtonView from '../button/listitembuttonview.js';
87
87
  *
88
88
  * @param locale The locale instance.
89
89
  * @param ButtonClassOrInstance The dropdown button view class. Needs to implement the
90
- * @param behaviorOptions Attributes for the default behavior of the dropdown.
91
- *
92
90
  * {@link module:ui/dropdown/button/dropdownbutton~DropdownButton} interface.
93
91
  * @returns The dropdown view instance.
94
92
  */
@@ -390,7 +388,7 @@ export function focusChildOnDropdownOpen(dropdownView, childSelectorCallback) {
390
388
  * experience.
391
389
  *
392
390
  * @error ui-dropdown-focus-child-on-open-child-missing-focus
393
- * @param {module:ui/view~View} view
391
+ * @param {module:ui/view~View} view Child to focus.
394
392
  */
395
393
  logWarning('ui-dropdown-focus-child-on-open-child-missing-focus', { view: childToFocus });
396
394
  }
@@ -525,23 +523,7 @@ function focusDropdownPanelOnOpen(dropdownView) {
525
523
  * @param locale
526
524
  */
527
525
  function bindViewCollectionItemsToDefinitions(dropdownView, listItems, definitions, locale) {
528
- // List item checkboxes have a reserved space for the check icon, so we need to know if there are any checkboxes in the list
529
- // to adjust the layout accordingly. It'd look weird if the items on the list were not aligned horizontally.
530
- //
531
- // Possible theoretical performance problem if many items are added one by one, as this will be called for each item.
532
- listItems.on('change', () => {
533
- // Filter-map. Check all items, leave only these that have buttons and return the buttons.
534
- const listItemButtons = [...listItems].reduce((acc, item) => {
535
- if (item instanceof ListItemView && item.children.first instanceof ListItemButtonView) {
536
- acc.push(item.children.first);
537
- }
538
- return acc;
539
- }, []);
540
- const hasAnyCheckboxOnList = listItemButtons.some(button => button.isToggleable);
541
- listItemButtons.forEach(item => {
542
- item.hasCheckSpace = hasAnyCheckboxOnList;
543
- });
544
- });
526
+ bindDropdownToggleableButtonsAlignment(listItems);
545
527
  listItems.bindTo(definitions).using(def => {
546
528
  if (def.type === 'separator') {
547
529
  return new ListSeparatorView(locale);
@@ -575,3 +557,98 @@ function bindViewCollectionItemsToDefinitions(dropdownView, listItems, definitio
575
557
  return null;
576
558
  });
577
559
  }
560
+ /**
561
+ * Sets up alignment handling for toggleable buttons in a dropdown list.
562
+ *
563
+ * Buttons in dropdowns have reserved space for a check icon when they are toggleable.
564
+ * When at least one button in the list is toggleable, all other buttons (even non-toggleable ones)
565
+ * will have space on their left side to align with toggleable buttons.
566
+ *
567
+ * This function handles a special case where a new toggleable button is added (or removed) to a list
568
+ * where previous buttons weren't toggleable. In that case, those previous buttons will
569
+ * automatically allocate space to align with the new toggleable button.
570
+ *
571
+ * Example:
572
+ * ```
573
+ * Before adding toggleable button:
574
+ * +----------------+
575
+ * | Normal Button |
576
+ * +----------------+
577
+ * | Another Button |
578
+ * +----------------+
579
+ *
580
+ * After adding toggleable button:
581
+ * +-------------------+
582
+ * | Normal Button |
583
+ * +-------------------+
584
+ * | Another Button |
585
+ * +-------------------+
586
+ * | ✓ Toggle Button |
587
+ * +-------------------+
588
+ * ```
589
+ *
590
+ * @param listItems Collection of list items to observe for toggleable buttons.
591
+ */
592
+ function bindDropdownToggleableButtonsAlignment(listItems) {
593
+ // Keep track of how many toggleable buttons are in the list.
594
+ let toggleableButtonsCount = 0;
595
+ // Helper function that checks if a view item is a list item button.
596
+ const pickListItemButtonIfPresent = (item) => {
597
+ // Check if the item is a ListItemView with a ListItemButtonView as its first child.
598
+ if (!(item instanceof ListItemView) || !(item.children.first instanceof ListItemButtonView)) {
599
+ return null;
600
+ }
601
+ return item.children.first;
602
+ };
603
+ // Helper function that checks if a view item is a toggleable button.
604
+ // Returns the button if it's toggleable - otherwise, returns null.
605
+ const pickListItemToggleableButtonIfPresent = (item) => {
606
+ const listItemButtonView = pickListItemButtonIfPresent(item);
607
+ // Only return buttons that are configured as toggleable.
608
+ if (!listItemButtonView || !listItemButtonView.isToggleable) {
609
+ return null;
610
+ }
611
+ return listItemButtonView;
612
+ };
613
+ // Updates all buttons in the list to either allocate space for check marks or not.
614
+ // This ensures all buttons are properly aligned regardless of their toggleable state.
615
+ const updateAllButtonsCheckSpace = (hasSpace) => {
616
+ for (const listItem of listItems) {
617
+ const listItemButton = pickListItemButtonIfPresent(listItem);
618
+ if (listItemButton) {
619
+ listItemButton.hasCheckSpace = hasSpace;
620
+ }
621
+ }
622
+ };
623
+ // Listen for changes in the list items collection.
624
+ listItems.on('change', (evt, data) => {
625
+ // Remember the current state - whether we have any toggleable buttons.
626
+ const prevToggleable = toggleableButtonsCount > 0;
627
+ // Process removed items - decrease count for each toggleable button removed.
628
+ for (const item of data.removed) {
629
+ if (pickListItemToggleableButtonIfPresent(item)) {
630
+ toggleableButtonsCount--;
631
+ }
632
+ }
633
+ // Process added items - increase count for each toggleable button added.
634
+ for (const item of data.added) {
635
+ const button = pickListItemButtonIfPresent(item);
636
+ if (!button) {
637
+ continue;
638
+ }
639
+ if (button.isToggleable) {
640
+ // Check if the button is toggleable and increase the count.
641
+ toggleableButtonsCount++;
642
+ }
643
+ // Depending on the current state, set the check space for the button.
644
+ button.hasCheckSpace = toggleableButtonsCount > 0;
645
+ }
646
+ // Check if the current state has changed.
647
+ const currentToggleable = toggleableButtonsCount > 0;
648
+ // Only update button alignment if we've crossed the threshold between
649
+ // having no toggleable buttons and having at least one.
650
+ if (prevToggleable !== currentToggleable) {
651
+ updateAllButtonsCheckSpace(currentToggleable);
652
+ }
653
+ });
654
+ }
@@ -13,7 +13,7 @@ import AriaLiveAnnouncer from '../arialiveannouncer.js';
13
13
  import type EditorUIView from './editoruiview.js';
14
14
  import type ToolbarView from '../toolbar/toolbarview.js';
15
15
  import { FocusTracker } from '@ckeditor/ckeditor5-utils';
16
- import type { Editor } from '@ckeditor/ckeditor5-core';
16
+ import type { Editor, ViewportOffsetConfig } from '@ckeditor/ckeditor5-core';
17
17
  import type { default as MenuBarView, MenuBarConfigAddedGroup, MenuBarConfigAddedItem, MenuBarConfigAddedMenu } from '../menubar/menubarview.js';
18
18
  declare const EditorUI_base: {
19
19
  new (): import("@ckeditor/ckeditor5-utils").Observable;
@@ -75,7 +75,8 @@ export default abstract class EditorUI extends /* #__PURE__ */ EditorUI_base {
75
75
  * top: 50,
76
76
  * right: 50,
77
77
  * bottom: 50,
78
- * left: 50
78
+ * left: 50,
79
+ * visualTop: 50
79
80
  * }
80
81
  * ```
81
82
  *
@@ -92,12 +93,7 @@ export default abstract class EditorUI extends /* #__PURE__ */ EditorUI_base {
92
93
  *
93
94
  * @observable
94
95
  */
95
- viewportOffset: {
96
- left?: number;
97
- right?: number;
98
- top?: number;
99
- bottom?: number;
100
- };
96
+ viewportOffset: ViewportOffset;
101
97
  /**
102
98
  * Stores all editable elements used by the editor instance.
103
99
  */
@@ -114,6 +110,10 @@ export default abstract class EditorUI extends /* #__PURE__ */ EditorUI_base {
114
110
  * The last focused element to which focus should return on `Esc` press.
115
111
  */
116
112
  private _lastFocusedForeignElement;
113
+ /**
114
+ * The DOM emitter instance used for visual viewport watching.
115
+ */
116
+ private _domEmitter?;
117
117
  /**
118
118
  * Creates an instance of the editor UI class.
119
119
  *
@@ -291,6 +291,19 @@ export default abstract class EditorUI extends /* #__PURE__ */ EditorUI_base {
291
291
  * Ensures that the focus tracker is aware of all views' DOM elements in the body collection.
292
292
  */
293
293
  private _bindBodyCollectionWithFocusTracker;
294
+ /**
295
+ * Set initial viewport offset and setup visualTop augmentation.
296
+ */
297
+ private _initViewportOffset;
298
+ /**
299
+ * Listen to visual viewport changes and update the viewportOffset with the visualTop property
300
+ * according to the visible part of it (visual viewport).
301
+ */
302
+ private _initVisualViewportSupport;
303
+ /**
304
+ * Calculate the viewport top offset according to the visible part of it (visual viewport).
305
+ */
306
+ private _getVisualViewportTopOffset;
294
307
  }
295
308
  /**
296
309
  * Fired when the editor UI is ready.
@@ -351,4 +364,12 @@ export interface FocusableToolbarOptions {
351
364
  */
352
365
  afterBlur?: () => void;
353
366
  }
367
+ export interface ViewportOffset extends ViewportOffsetConfig {
368
+ /**
369
+ * The top offset of the visual viewport.
370
+ *
371
+ * This value is calculated based on the visual viewport position.
372
+ */
373
+ visualTop?: number;
374
+ }
354
375
  export {};
@@ -11,7 +11,7 @@ import TooltipManager from '../tooltipmanager.js';
11
11
  import PoweredBy from './poweredby.js';
12
12
  import EvaluationBadge from './evaluationbadge.js';
13
13
  import AriaLiveAnnouncer from '../arialiveannouncer.js';
14
- import { ObservableMixin, isVisible, FocusTracker } from '@ckeditor/ckeditor5-utils';
14
+ import { ObservableMixin, DomEmitterMixin, global, isVisible, FocusTracker, getVisualViewportOffset } from '@ckeditor/ckeditor5-utils';
15
15
  import { normalizeMenuBarConfig } from '../menubar/utils.js';
16
16
  /**
17
17
  * A class providing the minimal interface that is required to successfully bootstrap any editor UI.
@@ -71,6 +71,10 @@ export default class EditorUI extends /* #__PURE__ */ ObservableMixin() {
71
71
  * The last focused element to which focus should return on `Esc` press.
72
72
  */
73
73
  _lastFocusedForeignElement = null;
74
+ /**
75
+ * The DOM emitter instance used for visual viewport watching.
76
+ */
77
+ _domEmitter;
74
78
  /**
75
79
  * Creates an instance of the editor UI class.
76
80
  *
@@ -86,7 +90,7 @@ export default class EditorUI extends /* #__PURE__ */ ObservableMixin() {
86
90
  this.poweredBy = new PoweredBy(editor);
87
91
  this.evaluationBadge = new EvaluationBadge(editor);
88
92
  this.ariaLiveAnnouncer = new AriaLiveAnnouncer(editor);
89
- this.set('viewportOffset', this._readViewportOffsetFromConfig());
93
+ this._initViewportOffset(this._readViewportOffsetFromConfig());
90
94
  this.once('ready', () => {
91
95
  this._bindBodyCollectionWithFocusTracker();
92
96
  this.isReady = true;
@@ -95,6 +99,7 @@ export default class EditorUI extends /* #__PURE__ */ ObservableMixin() {
95
99
  this.listenTo(editingView.document, 'layoutChanged', this.update.bind(this));
96
100
  this.listenTo(editingView, 'scrollToTheSelection', this._handleScrollToTheSelection.bind(this));
97
101
  this._initFocusTracking();
102
+ this._initVisualViewportSupport();
98
103
  }
99
104
  /**
100
105
  * The main (outermost) DOM element of the editor UI.
@@ -136,6 +141,9 @@ export default class EditorUI extends /* #__PURE__ */ ObservableMixin() {
136
141
  }
137
142
  this._editableElementsMap = new Map();
138
143
  this._focusableToolbarDefinitions = [];
144
+ if (this._domEmitter) {
145
+ this._domEmitter.stopListening();
146
+ }
139
147
  }
140
148
  /**
141
149
  * Stores the native DOM editable element used by the editor under a unique name.
@@ -279,7 +287,7 @@ export default class EditorUI extends /* #__PURE__ */ ObservableMixin() {
279
287
  * {@link module:ui/editorui/editorui~EditorUI#getEditableElement `getEditableElement()`} methods instead.
280
288
  *
281
289
  * @error editor-ui-deprecated-editable-elements
282
- * @param editorUI Editor UI instance the deprecated property belongs to.
290
+ * @param {module:ui/editorui/editorui~EditorUI} editorUI Editor UI instance the deprecated property belongs to.
283
291
  */
284
292
  console.warn('editor-ui-deprecated-editable-elements: ' +
285
293
  'The EditorUI#_editableElements property has been deprecated and will be removed in the near future.', { editorUI: this });
@@ -529,6 +537,51 @@ export default class EditorUI extends /* #__PURE__ */ ObservableMixin() {
529
537
  this.focusTracker.remove(view.element);
530
538
  });
531
539
  }
540
+ /**
541
+ * Set initial viewport offset and setup visualTop augmentation.
542
+ */
543
+ _initViewportOffset(viewportOffsetConfig) {
544
+ // Augment the viewport offset set from outside the editor with the visualTop property.
545
+ this.on('set:viewportOffset', (evt, name, value) => {
546
+ const visualTop = this._getVisualViewportTopOffset(value);
547
+ // Update only if there is a change in a value, so we do not trigger
548
+ // listeners to the viewportOffset observable.
549
+ if (value.visualTop !== visualTop) {
550
+ evt.return = { ...value, visualTop };
551
+ }
552
+ });
553
+ // Set the initial value after augmenting the setter.
554
+ this.set('viewportOffset', viewportOffsetConfig);
555
+ }
556
+ /**
557
+ * Listen to visual viewport changes and update the viewportOffset with the visualTop property
558
+ * according to the visible part of it (visual viewport).
559
+ */
560
+ _initVisualViewportSupport() {
561
+ if (!global.window.visualViewport) {
562
+ return;
563
+ }
564
+ const updateViewport = () => {
565
+ const visualTop = this._getVisualViewportTopOffset(this.viewportOffset);
566
+ // Update only if there is a change in a value, so we do not trigger
567
+ // listeners to the viewportOffset observable.
568
+ if (this.viewportOffset.visualTop !== visualTop) {
569
+ this.viewportOffset = { ...this.viewportOffset, visualTop };
570
+ }
571
+ };
572
+ // Listen to the changes in the visual viewport to adjust the visualTop of viewport offset.
573
+ this._domEmitter = new (DomEmitterMixin())();
574
+ this._domEmitter.listenTo(global.window.visualViewport, 'scroll', updateViewport);
575
+ this._domEmitter.listenTo(global.window.visualViewport, 'resize', updateViewport);
576
+ }
577
+ /**
578
+ * Calculate the viewport top offset according to the visible part of it (visual viewport).
579
+ */
580
+ _getVisualViewportTopOffset(viewportOffset) {
581
+ const visualViewportOffsetTop = getVisualViewportOffset().top;
582
+ const viewportTopOffset = viewportOffset.top || 0;
583
+ return visualViewportOffsetTop > viewportTopOffset ? 0 : viewportTopOffset - visualViewportOffsetTop;
584
+ }
532
585
  }
533
586
  /**
534
587
  * Returns a number (weight) for a toolbar definition. Visible toolbars have a higher priority and so do
@@ -217,8 +217,8 @@ export default class MenuBarView extends View {
217
217
  * {@link module:ui/menubar/menubarmenulistitembuttonview~MenuBarMenuListItemButtonView} (button).
218
218
  *
219
219
  * @error menu-bar-component-unsupported
220
- * @param componentName A name of the unsupported component used in the configuration.
221
- * @param componentView An unsupported component view.
220
+ * @param {string} componentName A name of the unsupported component used in the configuration.
221
+ * @param {module:ui/view~View} componentView An unsupported component view.
222
222
  */
223
223
  logWarning('menu-bar-component-unsupported', {
224
224
  componentName,
@@ -1142,8 +1142,8 @@ function handleAdditions(originalConfig, config, items) {
1142
1142
  * {@link module:core/editor/editorconfig~EditorConfig#menuBar menu bar configuration}.
1143
1143
  *
1144
1144
  * @error menu-bar-item-could-not-be-removed
1145
- * @param menuBarConfig The full configuration of the menu bar.
1146
- * @param itemName The name of the item that was not removed from the menu bar.
1145
+ * @param {object} menuBarConfig The full configuration of the menu bar.
1146
+ * @param {object} addedItemConfig The name of the item that was not removed from the menu bar.
1147
1147
  */
1148
1148
  logWarning('menu-bar-item-could-not-be-added', {
1149
1149
  menuBarConfig: originalConfig,
@@ -1222,9 +1222,9 @@ function purgeUnavailableComponents(originalConfig, config, componentFactory) {
1222
1222
  * menu bar item.
1223
1223
  *
1224
1224
  * @error menu-bar-item-unavailable
1225
- * @param menuBarConfig The full configuration of the menu bar.
1226
- * @param parentMenuConfig The config of the menu the unavailable component was defined in.
1227
- * @param componentName The name of the unavailable component.
1225
+ * @param {object} menuBarConfig The full configuration of the menu bar.
1226
+ * @param {object} parentMenuConfig The config of the menu the unavailable component was defined in.
1227
+ * @param {string} componentName The name of the unavailable component.
1228
1228
  */
1229
1229
  logWarning('menu-bar-item-unavailable', {
1230
1230
  menuBarConfig: originalConfig,
@@ -1303,8 +1303,8 @@ function warnAboutEmptyMenu(originalConfig, emptyMenuConfig, isUsingDefaultConfi
1303
1303
  * to account for the missing menu items.
1304
1304
  *
1305
1305
  * @error menu-bar-menu-empty
1306
- * @param menuBarConfig The full configuration of the menu bar.
1307
- * @param emptyMenuConfig The definition of the menu that has no child items.
1306
+ * @param {object} menuBarConfig The full configuration of the menu bar.
1307
+ * @param {object} emptyMenuConfig The definition of the menu that has no child items.
1308
1308
  */
1309
1309
  logWarning('menu-bar-menu-empty', {
1310
1310
  menuBarConfig: originalConfig,
@@ -6,7 +6,7 @@
6
6
  * @module ui/panel/balloon/balloonpanelview
7
7
  */
8
8
  import View from '../../view.js';
9
- import { getOptimalPosition, global, isRange, toUnit, isVisible, isText, ResizeObserver } from '@ckeditor/ckeditor5-utils';
9
+ import { getOptimalPosition, global, isRange, toUnit, isVisible, isText, ResizeObserver, Rect } from '@ckeditor/ckeditor5-utils';
10
10
  import { isElement } from 'es-toolkit/compat';
11
11
  import '../../../theme/components/panel/balloonpanel.css';
12
12
  const toPx = /* #__PURE__ */ toUnit('px');
@@ -563,18 +563,26 @@ class BalloonPanelView extends View {
563
563
  ...(config && { config })
564
564
  }),
565
565
  // ------- Sticky
566
- viewportStickyNorth: (targetRect, balloonRect, viewportRect, limiterRect) => {
567
- const boundaryRect = limiterRect || viewportRect;
568
- if (!targetRect.getIntersection(boundaryRect)) {
566
+ viewportStickyNorth: (targetRect, balloonRect, viewportRect) => {
567
+ // Get the intersection of the viewport and the document body.
568
+ const boundaryRect = new Rect(global.document.body).getIntersection(viewportRect.getVisible());
569
+ if (!boundaryRect) {
569
570
  return null;
570
571
  }
571
- // Engage when the target top and bottom edges are close or off the boundary.
572
- // By close, it means there's not enough space for the balloon arrow (offset).
573
- if (boundaryRect.height - targetRect.height > stickyVerticalOffset) {
572
+ // Get the visible intersection of the boundary and the document body.
573
+ const visibleBoundaryRect = boundaryRect.getVisible();
574
+ // Check if the target is in the boundary.
575
+ if (!targetRect.getIntersection(visibleBoundaryRect)) {
576
+ return null;
577
+ }
578
+ // Checks if there is enough space to put the balloon on the top or bottom of the target.
579
+ // If not, makes the balloon sticky.
580
+ if (!(visibleBoundaryRect.top - targetRect.top - stickyVerticalOffset < balloonRect.height &&
581
+ visibleBoundaryRect.bottom - targetRect.bottom < balloonRect.height)) {
574
582
  return null;
575
583
  }
576
584
  return {
577
- top: boundaryRect.top + stickyVerticalOffset,
585
+ top: visibleBoundaryRect.top + stickyVerticalOffset,
578
586
  left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
579
587
  name: 'arrowless',
580
588
  config: {
@@ -265,7 +265,10 @@ export default class ContextualBalloon extends Plugin {
265
265
  }
266
266
  // Don't modify the original options object.
267
267
  position = Object.assign({}, position, {
268
- viewportOffsetConfig: this.editor.ui.viewportOffset
268
+ viewportOffsetConfig: {
269
+ ...this.editor.ui.viewportOffset,
270
+ top: this.editor.ui.viewportOffset.visualTop
271
+ }
269
272
  });
270
273
  }
271
274
  return position;