@ckeditor/ckeditor5-utils 39.0.2 → 40.1.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/LICENSE.md CHANGED
@@ -2,7 +2,7 @@ Software License Agreement
2
2
  ==========================
3
3
 
4
4
  **CKEditor&nbsp;5 utilities** – https://github.com/ckeditor/ckeditor5-utils <br>
5
- Copyright (c) 2003-2023, [CKSource Holding sp. z o.o.](https://cksource.com) All rights reserved.
5
+ Copyright (c) 20032023, [CKSource Holding sp. z o.o.](https://cksource.com) All rights reserved.
6
6
 
7
7
  Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
8
8
 
@@ -13,9 +13,9 @@ Where not otherwise indicated, all CKEditor content is authored by CKSource engi
13
13
 
14
14
  The following libraries are included in CKEditor under the [MIT license](https://opensource.org/licenses/MIT):
15
15
 
16
- * lodash - Copyright (c) JS Foundation and other contributors https://js.foundation/. Based on Underscore.js, copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors http://underscorejs.org/.
16
+ * Lodash - Copyright (c) JS Foundation and other contributors https://js.foundation/. Based on Underscore.js, copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors http://underscorejs.org/.
17
17
 
18
18
  Trademarks
19
19
  ----------
20
20
 
21
- **CKEditor** is a trademark of [CKSource Holding sp. z o.o.](https://cksource.com) All other brand and product names are trademarks, registered trademarks or service marks of their respective holders.
21
+ **CKEditor** is a trademark of [CKSource Holding sp. z o.o.](https://cksource.com) All other brand and product names are trademarks, registered trademarks, or service marks of their respective holders.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-utils",
3
- "version": "39.0.2",
3
+ "version": "40.1.0",
4
4
  "description": "Miscellaneous utilities used by CKEditor 5.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -188,6 +188,12 @@ export default class Collection<T extends Record<string, any>> extends Collectio
188
188
  * @returns The result of mapping.
189
189
  */
190
190
  map<U>(callback: (item: T, index: number) => U, ctx?: any): Array<U>;
191
+ /**
192
+ * Performs the specified action for each item in the collection.
193
+ *
194
+ * @param ctx Context in which the `callback` will be called.
195
+ */
196
+ forEach(callback: (item: T, index: number) => unknown, ctx?: any): void;
191
197
  /**
192
198
  * Finds the first item in the collection for which the `callback` returns a true value.
193
199
  *
package/src/collection.js CHANGED
@@ -198,6 +198,14 @@ export default class Collection extends EmitterMixin() {
198
198
  map(callback, ctx) {
199
199
  return this._items.map(callback, ctx);
200
200
  }
201
+ /**
202
+ * Performs the specified action for each item in the collection.
203
+ *
204
+ * @param ctx Context in which the `callback` will be called.
205
+ */
206
+ forEach(callback, ctx) {
207
+ this._items.forEach(callback, ctx);
208
+ }
201
209
  /**
202
210
  * Finds the first item in the collection for which the `callback` returns a true value.
203
211
  *
package/src/config.js CHANGED
@@ -151,12 +151,13 @@ export default class Config {
151
151
  * Clones configuration object or value.
152
152
  */
153
153
  function cloneConfig(source) {
154
- return cloneDeepWith(source, leaveDOMReferences);
154
+ return cloneDeepWith(source, leaveItemReferences);
155
155
  }
156
156
  /**
157
157
  * A customized function for cloneDeepWith.
158
- * It will leave references to DOM Elements instead of cloning them.
158
+ * In case if it's a DOM Element it will leave references to DOM Elements instead of cloning them.
159
+ * If it's a function it will leave reference to actuall function.
159
160
  */
160
- function leaveDOMReferences(value) {
161
- return isElement(value) ? value : undefined;
161
+ function leaveItemReferences(value) {
162
+ return isElement(value) || typeof value === 'function' ? value : undefined;
162
163
  }
@@ -18,24 +18,6 @@ type SVGElementAttributes = HTMLElementAttributes & {
18
18
  * Element or elements that will be added to the created element as children. Strings will be automatically turned into Text nodes.
19
19
  */
20
20
  type ChildrenElements = Node | string | Iterable<Node | string>;
21
- /**
22
- * Creates an HTML element with attributes and children elements.
23
- *
24
- * ```ts
25
- * createElement( document, 'p' ); // <p>
26
- * createElement( document, 'p', { class: 'foo' } ); // <p class="foo">
27
- * createElement( document, 'p', null, 'foo' ); // <p>foo</p>
28
- * createElement( document, 'p', null, [ createElement(...) ] ); // <p><...></p>
29
- * ```
30
- *
31
- * @label HTML_ELEMENT
32
- * @param doc Document used to create the element.
33
- * @param name Name of the HTML element.
34
- * @param attributes Object where keys represent attribute keys and values represent attribute values.
35
- * @param children Child or any iterable of children. Strings will be automatically turned into Text nodes.
36
- * @returns HTML element.
37
- */
38
- export default function createElement<T extends keyof HTMLElementTagNameMap>(doc: Document, name: T, attributes?: HTMLElementAttributes, children?: ChildrenElements): HTMLElementTagNameMap[T];
39
21
  /**
40
22
  * Creates an SVG element with attributes and children elements.
41
23
  *
@@ -54,4 +36,22 @@ export default function createElement<T extends keyof HTMLElementTagNameMap>(doc
54
36
  * @returns SVG element.
55
37
  */
56
38
  export default function createElement<T extends keyof SVGElementTagNameMap>(doc: Document, name: T, attributes: SVGElementAttributes, children?: ChildrenElements): SVGElementTagNameMap[T];
39
+ /**
40
+ * Creates an HTML element with attributes and children elements.
41
+ *
42
+ * ```ts
43
+ * createElement( document, 'p' ); // <p>
44
+ * createElement( document, 'p', { class: 'foo' } ); // <p class="foo">
45
+ * createElement( document, 'p', null, 'foo' ); // <p>foo</p>
46
+ * createElement( document, 'p', null, [ createElement(...) ] ); // <p><...></p>
47
+ * ```
48
+ *
49
+ * @label HTML_ELEMENT
50
+ * @param doc Document used to create the element.
51
+ * @param name Name of the HTML element.
52
+ * @param attributes Object where keys represent attribute keys and values represent attribute values.
53
+ * @param children Child or any iterable of children. Strings will be automatically turned into Text nodes.
54
+ * @returns HTML element.
55
+ */
56
+ export default function createElement<T extends keyof HTMLElementTagNameMap>(doc: Document, name: T, attributes?: HTMLElementAttributes, children?: ChildrenElements): HTMLElementTagNameMap[T];
57
57
  export {};
@@ -8,6 +8,9 @@ import Rect, { type RectSource } from './rect';
8
8
  * target in the visually most efficient way, taking various restrictions like viewport or limiter geometry
9
9
  * into consideration.
10
10
  *
11
+ * **Note**: If there are no position coordinates found that meet the requirements (arguments of this helper),
12
+ * `null` is returned.
13
+ *
11
14
  * ```ts
12
15
  * // The element which is to be positioned.
13
16
  * const element = document.body.querySelector( '#toolbar' );
@@ -68,7 +71,7 @@ import Rect, { type RectSource } from './rect';
68
71
  *
69
72
  * @param options The input data and configuration of the helper.
70
73
  */
71
- export declare function getOptimalPosition({ element, target, positions, limiter, fitInViewport, viewportOffsetConfig }: Options): Position;
74
+ export declare function getOptimalPosition({ element, target, positions, limiter, fitInViewport, viewportOffsetConfig }: Options): Position | null;
72
75
  /**
73
76
  * A position object which instances are created and used by the {@link module:utils/dom/position~getOptimalPosition} helper.
74
77
  *
@@ -179,7 +182,7 @@ export interface Options {
179
182
  * @param viewportRect The rect of the visual browser viewport.
180
183
  * @returns When the function returns `null`, it will not be considered by {@link module:utils/dom/position~getOptimalPosition}.
181
184
  */
182
- export type PositioningFunction = (elementRect: Rect, targetRect: Rect, viewportRect: Rect | null) => PositioningFunctionResult | null;
185
+ export type PositioningFunction = (elementRect: Rect, targetRect: Rect, viewportRect: Rect, limiterRect?: Rect) => PositioningFunctionResult | null;
183
186
  /**
184
187
  * The result of {@link module:utils/dom/position~PositioningFunction}.
185
188
  */
@@ -8,14 +8,47 @@
8
8
  import global from './global';
9
9
  import Rect from './rect';
10
10
  import getPositionedAncestor from './getpositionedancestor';
11
- import getBorderWidths from './getborderwidths';
12
11
  import { isFunction } from 'lodash-es';
13
- // @if CK_DEBUG_POSITION // const RectDrawer = require( '@ckeditor/ckeditor5-utils/tests/_utils/rectdrawer' ).default
12
+ // @if CK_DEBUG_POSITION // const {
13
+ // @if CK_DEBUG_POSITION // default: RectDrawer,
14
+ // @if CK_DEBUG_POSITION // diagonalStylesBlack,
15
+ // @if CK_DEBUG_POSITION // diagonalStylesGreen,
16
+ // @if CK_DEBUG_POSITION // diagonalStylesRed
17
+ // @if CK_DEBUG_POSITION // } = require( '@ckeditor/ckeditor5-utils/tests/_utils/rectdrawer' );
18
+ // @if CK_DEBUG_POSITION // const TARGET_RECT_STYLE = {
19
+ // @if CK_DEBUG_POSITION // outlineWidth: '2px', outlineStyle: 'dashed', outlineColor: 'blue', outlineOffset: '2px'
20
+ // @if CK_DEBUG_POSITION // };
21
+ // @if CK_DEBUG_POSITION // const VISIBLE_TARGET_RECT_STYLE = {
22
+ // @if CK_DEBUG_POSITION // ...diagonalStylesBlack,
23
+ // @if CK_DEBUG_POSITION // opacity: '1',
24
+ // @if CK_DEBUG_POSITION // backgroundColor: '#00000033',
25
+ // @if CK_DEBUG_POSITION // outlineWidth: '2px'
26
+ // @if CK_DEBUG_POSITION // };
27
+ // @if CK_DEBUG_POSITION // const VIEWPORT_RECT_STYLE = {
28
+ // @if CK_DEBUG_POSITION // outlineWidth: '2px',
29
+ // @if CK_DEBUG_POSITION // outlineOffset: '-2px',
30
+ // @if CK_DEBUG_POSITION // outlineStyle: 'solid',
31
+ // @if CK_DEBUG_POSITION // outlineColor: 'red'
32
+ // @if CK_DEBUG_POSITION // };
33
+ // @if CK_DEBUG_POSITION // const VISIBLE_LIMITER_RECT_STYLE = {
34
+ // @if CK_DEBUG_POSITION // ...diagonalStylesGreen,
35
+ // @if CK_DEBUG_POSITION // outlineWidth: '2px',
36
+ // @if CK_DEBUG_POSITION // outlineOffset: '-2px'
37
+ // @if CK_DEBUG_POSITION // };
38
+ // @if CK_DEBUG_POSITION // const ELEMENT_RECT_STYLE = {
39
+ // @if CK_DEBUG_POSITION // outlineWidth: '2px', outlineColor: 'orange', outlineOffset: '-2px'
40
+ // @if CK_DEBUG_POSITION // };
41
+ // @if CK_DEBUG_POSITION // const CHOSEN_POSITION_RECT_STYLE = {
42
+ // @if CK_DEBUG_POSITION // opacity: .5, outlineColor: 'magenta', backgroundColor: 'magenta'
43
+ // @if CK_DEBUG_POSITION // };
14
44
  /**
15
45
  * Calculates the `position: absolute` coordinates of a given element so it can be positioned with respect to the
16
46
  * target in the visually most efficient way, taking various restrictions like viewport or limiter geometry
17
47
  * into consideration.
18
48
  *
49
+ * **Note**: If there are no position coordinates found that meet the requirements (arguments of this helper),
50
+ * `null` is returned.
51
+ *
19
52
  * ```ts
20
53
  * // The element which is to be positioned.
21
54
  * const element = document.body.querySelector( '#toolbar' );
@@ -88,32 +121,56 @@ export function getOptimalPosition({ element, target, positions, limiter, fitInV
88
121
  limiter = limiter();
89
122
  }
90
123
  const positionedElementAncestor = getPositionedAncestor(element);
124
+ const constrainedViewportRect = getConstrainedViewportRect(viewportOffsetConfig);
91
125
  const elementRect = new Rect(element);
92
- const targetRect = new Rect(target);
126
+ const visibleTargetRect = getVisibleViewportIntersectionRect(target, constrainedViewportRect);
93
127
  let bestPosition;
128
+ // @if CK_DEBUG_POSITION // const targetRect = new Rect( target );
94
129
  // @if CK_DEBUG_POSITION // RectDrawer.clear();
95
- // @if CK_DEBUG_POSITION // RectDrawer.draw( targetRect, { outlineWidth: '5px' }, 'Target' );
96
- const viewportRect = fitInViewport && getConstrainedViewportRect(viewportOffsetConfig) || null;
97
- const positionOptions = { targetRect, elementRect, positionedElementAncestor, viewportRect };
130
+ // @if CK_DEBUG_POSITION // RectDrawer.draw( targetRect, TARGET_RECT_STYLE, 'Target' );
131
+ // @if CK_DEBUG_POSITION // if ( constrainedViewportRect ) {
132
+ // @if CK_DEBUG_POSITION // RectDrawer.draw( constrainedViewportRect, VIEWPORT_RECT_STYLE, 'Viewport' );
133
+ // @if CK_DEBUG_POSITION // }
134
+ // If the target got cropped by ancestors or went off the screen, positioning does not make any sense.
135
+ if (!visibleTargetRect || !constrainedViewportRect.getIntersection(visibleTargetRect)) {
136
+ return null;
137
+ }
138
+ // @if CK_DEBUG_POSITION // RectDrawer.draw( visibleTargetRect, VISIBLE_TARGET_RECT_STYLE, 'VisTgt' );
139
+ const positionOptions = {
140
+ targetRect: visibleTargetRect,
141
+ elementRect,
142
+ positionedElementAncestor,
143
+ viewportRect: constrainedViewportRect
144
+ };
98
145
  // If there are no limits, just grab the very first position and be done with that drama.
99
146
  if (!limiter && !fitInViewport) {
100
147
  bestPosition = new PositionObject(positions[0], positionOptions);
101
148
  }
102
149
  else {
103
- const limiterRect = limiter && new Rect(limiter).getVisible();
104
- // @if CK_DEBUG_POSITION // if ( viewportRect ) {
105
- // @if CK_DEBUG_POSITION // RectDrawer.draw( viewportRect, { outlineWidth: '5px' }, 'Viewport' );
106
- // @if CK_DEBUG_POSITION // }
107
- // @if CK_DEBUG_POSITION // if ( limiter ) {
108
- // @if CK_DEBUG_POSITION // RectDrawer.draw( limiterRect, { outlineWidth: '5px', outlineColor: 'green' }, 'Visible limiter' );
109
- // @if CK_DEBUG_POSITION // }
110
- Object.assign(positionOptions, { limiterRect, viewportRect });
150
+ if (limiter) {
151
+ const visibleLimiterRect = getVisibleViewportIntersectionRect(limiter, constrainedViewportRect);
152
+ if (visibleLimiterRect) {
153
+ positionOptions.limiterRect = visibleLimiterRect;
154
+ // @if CK_DEBUG_POSITION // RectDrawer.draw( visibleLimiterRect, VISIBLE_LIMITER_RECT_STYLE, 'VisLim' );
155
+ }
156
+ }
111
157
  // If there's no best position found, i.e. when all intersections have no area because
112
- // rects have no width or height, then just use the first available position.
113
- bestPosition = getBestPosition(positions, positionOptions) || new PositionObject(positions[0], positionOptions);
158
+ // rects have no width or height, then just return `null`
159
+ bestPosition = getBestPosition(positions, positionOptions);
114
160
  }
115
161
  return bestPosition;
116
162
  }
163
+ /**
164
+ * Returns intersection of visible source `Rect` with Viewport `Rect`. In case when source `Rect` is not visible
165
+ * or there is no intersection between source `Rect` and Viewport `Rect`, `null` will be returned.
166
+ */
167
+ function getVisibleViewportIntersectionRect(source, viewportRect) {
168
+ const visibleSourceRect = new Rect(source).getVisible();
169
+ if (!visibleSourceRect) {
170
+ return null;
171
+ }
172
+ return visibleSourceRect.getIntersection(viewportRect);
173
+ }
117
174
  /**
118
175
  * Returns a viewport `Rect` shrunk by the viewport offset config from all sides.
119
176
  */
@@ -145,61 +202,30 @@ function getBestPosition(positions, options) {
145
202
  // If a such position is found that element is fully contained by the limiter then, obviously,
146
203
  // there will be no better one, so finishing.
147
204
  if (limiterIntersectionArea === elementRectArea) {
205
+ // @if CK_DEBUG_POSITION // RectDrawer.draw( position._rect, CHOSEN_POSITION_RECT_STYLE, [
206
+ // @if CK_DEBUG_POSITION // position.name,
207
+ // @if CK_DEBUG_POSITION // '100% fit',
208
+ // @if CK_DEBUG_POSITION // ].join( '\n' ) );
148
209
  return position;
149
210
  }
150
211
  // To maximize both viewport and limiter intersection areas we use distance on _viewportIntersectionArea
151
212
  // and _limiterIntersectionArea plane (without sqrt because we are looking for max value).
152
213
  const fitFactor = viewportIntersectionArea ** 2 + limiterIntersectionArea ** 2;
214
+ // @if CK_DEBUG_POSITION // RectDrawer.draw( position._rect, { opacity: .4 }, [
215
+ // @if CK_DEBUG_POSITION // position.name,
216
+ // @if CK_DEBUG_POSITION // 'Vi=' + Math.round( viewportIntersectionArea ),
217
+ // @if CK_DEBUG_POSITION // 'Li=' + Math.round( limiterIntersectionArea )
218
+ // @if CK_DEBUG_POSITION // ].join( '\n' ) );
153
219
  if (fitFactor > maxFitFactor) {
154
220
  maxFitFactor = fitFactor;
155
221
  bestPosition = position;
156
222
  }
157
223
  }
224
+ // @if CK_DEBUG_POSITION // if ( bestPosition ) {
225
+ // @if CK_DEBUG_POSITION // RectDrawer.draw( bestPosition._rect, CHOSEN_POSITION_RECT_STYLE );
226
+ // @if CK_DEBUG_POSITION // }
158
227
  return bestPosition;
159
228
  }
160
- /**
161
- * For a given absolute Rect coordinates object and a positioned element ancestor, it updates its
162
- * coordinates that make up for the position and the scroll of the ancestor.
163
- *
164
- * This is necessary because while Rects (and DOMRects) are relative to the browser's viewport, their coordinates
165
- * are used in real–life to position elements with `position: absolute`, which are scoped by any positioned
166
- * (and scrollable) ancestors.
167
- */
168
- function shiftRectToCompensatePositionedAncestor(rect, positionedElementAncestor) {
169
- const ancestorPosition = getRectForAbsolutePositioning(new Rect(positionedElementAncestor));
170
- const ancestorBorderWidths = getBorderWidths(positionedElementAncestor);
171
- let moveX = 0;
172
- let moveY = 0;
173
- // (https://github.com/ckeditor/ckeditor5-ui-default/issues/126)
174
- // If there's some positioned ancestor of the panel, then its `Rect` must be taken into
175
- // consideration. `Rect` is always relative to the viewport while `position: absolute` works
176
- // with respect to that positioned ancestor.
177
- moveX -= ancestorPosition.left;
178
- moveY -= ancestorPosition.top;
179
- // (https://github.com/ckeditor/ckeditor5-utils/issues/139)
180
- // If there's some positioned ancestor of the panel, not only its position must be taken into
181
- // consideration (see above) but also its internal scrolls. Scroll have an impact here because `Rect`
182
- // is relative to the viewport (it doesn't care about scrolling), while `position: absolute`
183
- // must compensate that scrolling.
184
- moveX += positionedElementAncestor.scrollLeft;
185
- moveY += positionedElementAncestor.scrollTop;
186
- // (https://github.com/ckeditor/ckeditor5-utils/issues/139)
187
- // If there's some positioned ancestor of the panel, then its `Rect` includes its CSS `borderWidth`
188
- // while `position: absolute` positioning does not consider it.
189
- // E.g. `{ position: absolute, top: 0, left: 0 }` means upper left corner of the element,
190
- // not upper-left corner of its border.
191
- moveX -= ancestorBorderWidths.left;
192
- moveY -= ancestorBorderWidths.top;
193
- rect.moveBy(moveX, moveY);
194
- }
195
- /**
196
- * DOMRect (also Rect) works in a scroll–independent geometry but `position: absolute` doesn't.
197
- * This function converts Rect to `position: absolute` coordinates.
198
- */
199
- function getRectForAbsolutePositioning(rect) {
200
- const { scrollX, scrollY } = global.window;
201
- return rect.clone().moveBy(scrollX, scrollY);
202
- }
203
229
  /**
204
230
  * A position class which instances are created and used by the {@link module:utils/dom/position~getOptimalPosition} helper.
205
231
  *
@@ -221,7 +247,7 @@ class PositionObject {
221
247
  * @param options.positionedElementAncestor Nearest element ancestor element which CSS position is not "static".
222
248
  */
223
249
  constructor(positioningFunction, options) {
224
- const positioningFunctionOutput = positioningFunction(options.targetRect, options.elementRect, options.viewportRect);
250
+ const positioningFunctionOutput = positioningFunction(options.targetRect, options.elementRect, options.viewportRect, options.limiterRect);
225
251
  // Nameless position for a function that didn't participate.
226
252
  if (!positioningFunctionOutput) {
227
253
  return;
@@ -229,7 +255,7 @@ class PositionObject {
229
255
  const { left, top, name, config } = positioningFunctionOutput;
230
256
  this.name = name;
231
257
  this.config = config;
232
- this._positioningFunctionCorrdinates = { left, top };
258
+ this._positioningFunctionCoordinates = { left, top };
233
259
  this._options = options;
234
260
  }
235
261
  /**
@@ -252,19 +278,7 @@ class PositionObject {
252
278
  get limiterIntersectionArea() {
253
279
  const limiterRect = this._options.limiterRect;
254
280
  if (limiterRect) {
255
- const viewportRect = this._options.viewportRect;
256
- if (viewportRect) {
257
- // Consider only the part of the limiter which is visible in the viewport. So the limiter is getting limited.
258
- const limiterViewportIntersectRect = limiterRect.getIntersection(viewportRect);
259
- if (limiterViewportIntersectRect) {
260
- // If the limiter is within the viewport, then check the intersection between that part of the
261
- // limiter and actual position.
262
- return limiterViewportIntersectRect.getIntersectionArea(this._rect);
263
- }
264
- }
265
- else {
266
- return limiterRect.getIntersectionArea(this._rect);
267
- }
281
+ return limiterRect.getIntersectionArea(this._rect);
268
282
  }
269
283
  return 0;
270
284
  }
@@ -273,10 +287,7 @@ class PositionObject {
273
287
  */
274
288
  get viewportIntersectionArea() {
275
289
  const viewportRect = this._options.viewportRect;
276
- if (viewportRect) {
277
- return viewportRect.getIntersectionArea(this._rect);
278
- }
279
- return 0;
290
+ return viewportRect.getIntersectionArea(this._rect);
280
291
  }
281
292
  /**
282
293
  * An already positioned element rect. A clone of the element rect passed to the constructor
@@ -286,7 +297,7 @@ class PositionObject {
286
297
  if (this._cachedRect) {
287
298
  return this._cachedRect;
288
299
  }
289
- this._cachedRect = this._options.elementRect.clone().moveTo(this._positioningFunctionCorrdinates.left, this._positioningFunctionCorrdinates.top);
300
+ this._cachedRect = this._options.elementRect.clone().moveTo(this._positioningFunctionCoordinates.left, this._positioningFunctionCoordinates.top);
290
301
  return this._cachedRect;
291
302
  }
292
303
  /**
@@ -296,10 +307,7 @@ class PositionObject {
296
307
  if (this._cachedAbsoluteRect) {
297
308
  return this._cachedAbsoluteRect;
298
309
  }
299
- this._cachedAbsoluteRect = getRectForAbsolutePositioning(this._rect);
300
- if (this._options.positionedElementAncestor) {
301
- shiftRectToCompensatePositionedAncestor(this._cachedAbsoluteRect, this._options.positionedElementAncestor);
302
- }
310
+ this._cachedAbsoluteRect = this._rect.toAbsoluteRect();
303
311
  return this._cachedAbsoluteRect;
304
312
  }
305
313
  }
package/src/dom/rect.d.ts CHANGED
@@ -117,7 +117,7 @@ export default class Rect {
117
117
  */
118
118
  getArea(): number;
119
119
  /**
120
- * Returns a new rect, a part of the original rect, which is actually visible to the user,
120
+ * Returns a new rect, a part of the original rect, which is actually visible to the user and is relative to the,`body`,
121
121
  * e.g. an original rect cropped by parent element rects which have `overflow` set in CSS
122
122
  * other than `"visible"`.
123
123
  *
@@ -150,6 +150,10 @@ export default class Rect {
150
150
  * @returns `true` if contains, `false` otherwise.
151
151
  */
152
152
  contains(anotherRect: Rect): boolean;
153
+ /**
154
+ * Recalculates screen coordinates to coordinates relative to the positioned ancestor offset.
155
+ */
156
+ toAbsoluteRect(): Rect;
153
157
  /**
154
158
  * Excludes scrollbars and CSS borders from the rect.
155
159
  *
package/src/dom/rect.js CHANGED
@@ -9,6 +9,8 @@ import isRange from './isrange';
9
9
  import isWindow from './iswindow';
10
10
  import getBorderWidths from './getborderwidths';
11
11
  import isText from './istext';
12
+ import getPositionedAncestor from './getpositionedancestor';
13
+ import global from './global';
12
14
  const rectProperties = ['top', 'right', 'bottom', 'left', 'width', 'height'];
13
15
  /**
14
16
  * A helper class representing a `ClientRect` object, e.g. value returned by
@@ -141,7 +143,9 @@ export default class Rect {
141
143
  return null;
142
144
  }
143
145
  else {
144
- return new Rect(rect);
146
+ const newRect = new Rect(rect);
147
+ newRect._source = this._source;
148
+ return newRect;
145
149
  }
146
150
  }
147
151
  /**
@@ -165,7 +169,7 @@ export default class Rect {
165
169
  return this.width * this.height;
166
170
  }
167
171
  /**
168
- * Returns a new rect, a part of the original rect, which is actually visible to the user,
172
+ * Returns a new rect, a part of the original rect, which is actually visible to the user and is relative to the,`body`,
169
173
  * e.g. an original rect cropped by parent element rects which have `overflow` set in CSS
170
174
  * other than `"visible"`.
171
175
  *
@@ -193,14 +197,47 @@ export default class Rect {
193
197
  let absolutelyPositionedChildElement;
194
198
  // Check the ancestors all the way up to the <body>.
195
199
  while (parent && !isBody(parent)) {
200
+ const isParentOverflowVisible = getElementOverflow(parent) === 'visible';
196
201
  if (child instanceof HTMLElement && getElementPosition(child) === 'absolute') {
197
202
  absolutelyPositionedChildElement = child;
198
203
  }
204
+ const parentElementPosition = getElementPosition(parent);
199
205
  // The child will be cropped only if it has `position: absolute` and the parent has `position: relative` + some overflow.
200
206
  // Otherwise there's no chance of visual clipping and the parent can be skipped
201
207
  // https://github.com/ckeditor/ckeditor5/issues/14107.
202
- if (absolutelyPositionedChildElement &&
203
- (getElementPosition(parent) !== 'relative' || getElementOverflow(parent) === 'visible')) {
208
+ //
209
+ // condition: isParentOverflowVisible
210
+ // +---------------------------+
211
+ // | #parent |
212
+ // | (overflow: visible) |
213
+ // | +-----------+---------------+
214
+ // | | child |
215
+ // | +-----------+---------------+
216
+ // +---------------------------+
217
+ //
218
+ // condition: absolutelyPositionedChildElement && parentElementPosition === 'relative' && isParentOverflowVisible
219
+ // +---------------------------+
220
+ // | parent |
221
+ // | (position: relative;) |
222
+ // | (overflow: visible;) |
223
+ // | +-----------+---------------+
224
+ // | | child |
225
+ // | | (position: absolute;) |
226
+ // | +-----------+---------------+
227
+ // +---------------------------+
228
+ //
229
+ // condition: absolutelyPositionedChildElement && parentElementPosition !== 'relative'
230
+ // +---------------------------+
231
+ // | parent |
232
+ // | (position: static;) |
233
+ // | +-----------+---------------+
234
+ // | | child |
235
+ // | | (position: absolute;) |
236
+ // | +-----------+---------------+
237
+ // +---------------------------+
238
+ if (isParentOverflowVisible ||
239
+ absolutelyPositionedChildElement && ((parentElementPosition === 'relative' && isParentOverflowVisible) ||
240
+ parentElementPosition !== 'relative')) {
204
241
  child = parent;
205
242
  parent = parent.parentNode;
206
243
  continue;
@@ -248,6 +285,20 @@ export default class Rect {
248
285
  const intersectRect = this.getIntersection(anotherRect);
249
286
  return !!(intersectRect && intersectRect.isEqual(anotherRect));
250
287
  }
288
+ /**
289
+ * Recalculates screen coordinates to coordinates relative to the positioned ancestor offset.
290
+ */
291
+ toAbsoluteRect() {
292
+ const { scrollX, scrollY } = global.window;
293
+ const absoluteRect = this.clone().moveBy(scrollX, scrollY);
294
+ if (isDomElement(absoluteRect._source)) {
295
+ const positionedAncestor = getPositionedAncestor(absoluteRect._source);
296
+ if (positionedAncestor) {
297
+ shiftRectToCompensatePositionedAncestor(absoluteRect, positionedAncestor);
298
+ }
299
+ }
300
+ return absoluteRect;
301
+ }
251
302
  /**
252
303
  * Excludes scrollbars and CSS borders from the rect.
253
304
  *
@@ -378,11 +429,46 @@ function isDomElement(value) {
378
429
  * Returns the value of the `position` style of an `HTMLElement`.
379
430
  */
380
431
  function getElementPosition(element) {
381
- return element.ownerDocument.defaultView.getComputedStyle(element).position;
432
+ return element instanceof HTMLElement ? element.ownerDocument.defaultView.getComputedStyle(element).position : 'static';
382
433
  }
383
434
  /**
384
- * Returns the value of the `overflow` style of an `HTMLElement`.
435
+ * Returns the value of the `overflow` style of an `HTMLElement` or a `Range`.
385
436
  */
386
437
  function getElementOverflow(element) {
387
- return element.ownerDocument.defaultView.getComputedStyle(element).overflow;
438
+ return element instanceof HTMLElement ? element.ownerDocument.defaultView.getComputedStyle(element).overflow : 'visible';
439
+ }
440
+ /**
441
+ * For a given absolute Rect coordinates object and a positioned element ancestor, it updates its
442
+ * coordinates that make up for the position and the scroll of the ancestor.
443
+ *
444
+ * This is necessary because while Rects (and DOMRects) are relative to the browser's viewport, their coordinates
445
+ * are used in real–life to position elements with `position: absolute`, which are scoped by any positioned
446
+ * (and scrollable) ancestors.
447
+ */
448
+ function shiftRectToCompensatePositionedAncestor(rect, positionedElementAncestor) {
449
+ const ancestorPosition = new Rect(positionedElementAncestor);
450
+ const ancestorBorderWidths = getBorderWidths(positionedElementAncestor);
451
+ let moveX = 0;
452
+ let moveY = 0;
453
+ // (https://github.com/ckeditor/ckeditor5-ui-default/issues/126)
454
+ // If there's some positioned ancestor of the panel, then its `Rect` must be taken into
455
+ // consideration. `Rect` is always relative to the viewport while `position: absolute` works
456
+ // with respect to that positioned ancestor.
457
+ moveX -= ancestorPosition.left;
458
+ moveY -= ancestorPosition.top;
459
+ // (https://github.com/ckeditor/ckeditor5-utils/issues/139)
460
+ // If there's some positioned ancestor of the panel, not only its position must be taken into
461
+ // consideration (see above) but also its internal scrolls. Scroll have an impact here because `Rect`
462
+ // is relative to the viewport (it doesn't care about scrolling), while `position: absolute`
463
+ // must compensate that scrolling.
464
+ moveX += positionedElementAncestor.scrollLeft;
465
+ moveY += positionedElementAncestor.scrollTop;
466
+ // (https://github.com/ckeditor/ckeditor5-utils/issues/139)
467
+ // If there's some positioned ancestor of the panel, then its `Rect` includes its CSS `borderWidth`
468
+ // while `position: absolute` positioning does not consider it.
469
+ // E.g. `{ position: absolute, top: 0, left: 0 }` means upper left corner of the element,
470
+ // not upper-left corner of its border.
471
+ moveX -= ancestorBorderWidths.left;
472
+ moveY -= ancestorBorderWidths.top;
473
+ rect.moveBy(moveX, moveY);
388
474
  }
package/src/index.d.ts CHANGED
@@ -25,9 +25,8 @@ export { default as DomEmitterMixin, type DomEmitter } from './dom/emittermixin'
25
25
  export { default as findClosestScrollableAncestor } from './dom/findclosestscrollableancestor';
26
26
  export { default as global } from './dom/global';
27
27
  export { default as getAncestors } from './dom/getancestors';
28
- export { default as getElementsIntersectionRect } from './dom/getelementsintersectionrect';
29
- export { default as getScrollableAncestors } from './dom/getscrollableancestors';
30
28
  export { default as getDataFromElement } from './dom/getdatafromelement';
29
+ export { default as getBorderWidths } from './dom/getborderwidths';
31
30
  export { default as isText } from './dom/istext';
32
31
  export { default as Rect, type RectSource } from './dom/rect';
33
32
  export { default as ResizeObserver } from './dom/resizeobserver';
package/src/index.js CHANGED
@@ -24,9 +24,8 @@ export { default as DomEmitterMixin } from './dom/emittermixin';
24
24
  export { default as findClosestScrollableAncestor } from './dom/findclosestscrollableancestor';
25
25
  export { default as global } from './dom/global';
26
26
  export { default as getAncestors } from './dom/getancestors';
27
- export { default as getElementsIntersectionRect } from './dom/getelementsintersectionrect';
28
- export { default as getScrollableAncestors } from './dom/getscrollableancestors';
29
27
  export { default as getDataFromElement } from './dom/getdatafromelement';
28
+ export { default as getBorderWidths } from './dom/getborderwidths';
30
29
  export { default as isText } from './dom/istext';
31
30
  export { default as Rect } from './dom/rect';
32
31
  export { default as ResizeObserver } from './dom/resizeobserver';
package/src/keyboard.js CHANGED
@@ -100,7 +100,7 @@ export function parseKeystroke(keystroke) {
100
100
  */
101
101
  export function getEnvKeystrokeText(keystroke) {
102
102
  let keystrokeCode = parseKeystroke(keystroke);
103
- const modifiersToGlyphs = Object.entries(env.isMac ? modifiersToGlyphsMac : modifiersToGlyphsNonMac);
103
+ const modifiersToGlyphs = Object.entries((env.isMac || env.isiOS) ? modifiersToGlyphsMac : modifiersToGlyphsNonMac);
104
104
  const modifiers = modifiersToGlyphs.reduce((modifiers, [name, glyph]) => {
105
105
  // Modifier keys are stored as a bit mask so extract those from the keystroke code.
106
106
  if ((keystrokeCode & keyCodes[name]) != 0) {
@@ -161,7 +161,7 @@ function getEnvKeyCode(key) {
161
161
  return getCode(key.slice(0, -1));
162
162
  }
163
163
  const code = getCode(key);
164
- return env.isMac && code == keyCodes.ctrl ? keyCodes.cmd : code;
164
+ return (env.isMac || env.isiOS) && code == keyCodes.ctrl ? keyCodes.cmd : code;
165
165
  }
166
166
  /**
167
167
  * Determines if the provided key code moves the {@link module:engine/model/documentselection~DocumentSelection selection}
@@ -23,4 +23,4 @@
23
23
  *
24
24
  * @returns New spliced array.
25
25
  */
26
- export default function spliceArray<T>(target: Array<T>, source: Array<T>, start: number, count: number): Array<T>;
26
+ export default function spliceArray<T>(target: ReadonlyArray<T>, source: ReadonlyArray<T>, start: number, count: number): Array<T>;
package/src/version.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
- declare const version = "39.0.2";
5
+ declare const version = "40.1.0";
6
6
  export default version;
7
7
  export declare const releaseDate: Date;
8
8
  declare global {
package/src/version.js CHANGED
@@ -6,10 +6,10 @@
6
6
  * @module utils/version
7
7
  */
8
8
  import CKEditorError from './ckeditorerror';
9
- const version = '39.0.2';
9
+ const version = '40.1.0';
10
10
  export default version;
11
11
  // The second argument is not a month. It is `monthIndex` and starts from `0`.
12
- export const releaseDate = new Date(2023, 8, 6);
12
+ export const releaseDate = new Date(2023, 10, 15);
13
13
  /* istanbul ignore next -- @preserve */
14
14
  if (globalThis.CKEDITOR_VERSION) {
15
15
  /**
@@ -1,14 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, 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 from './rect';
6
- /**
7
- * Calculates the intersection `Rect` of a given set of elements (and/or a `document`).
8
- * Also, takes into account the viewport top offset configuration.
9
- *
10
- * @internal
11
- * @param elements
12
- * @param viewportTopOffset
13
- */
14
- export default function getElementsIntersectionRect(elements: Array<HTMLElement | Document>, viewportTopOffset?: number): Rect | null;
@@ -1,43 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, 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 utils/dom/getelementsintersectionrect
7
- */
8
- import global from './global';
9
- import Rect from './rect';
10
- /**
11
- * Calculates the intersection `Rect` of a given set of elements (and/or a `document`).
12
- * Also, takes into account the viewport top offset configuration.
13
- *
14
- * @internal
15
- * @param elements
16
- * @param viewportTopOffset
17
- */
18
- export default function getElementsIntersectionRect(elements, viewportTopOffset = 0) {
19
- const elementRects = elements.map(element => {
20
- // The document (window) is yet another "element", but cropped by the top offset.
21
- if (element instanceof Document) {
22
- const windowRect = new Rect(global.window);
23
- windowRect.top += viewportTopOffset;
24
- windowRect.height -= viewportTopOffset;
25
- return windowRect;
26
- }
27
- else {
28
- return new Rect(element);
29
- }
30
- });
31
- let intersectionRect = elementRects[0];
32
- // @if CK_DEBUG_GETELEMENTSINTERSECTIONRECT // for ( const rect of elementRects ) {
33
- // @if CK_DEBUG_GETELEMENTSINTERSECTIONRECT // RectDrawer.draw( rect, {
34
- // @if CK_DEBUG_GETELEMENTSINTERSECTIONRECT // outlineWidth: '1px', opacity: '.7', outlineStyle: 'dashed'
35
- // @if CK_DEBUG_GETELEMENTSINTERSECTIONRECT // }, 'Scrollable element' );
36
- // @if CK_DEBUG_GETELEMENTSINTERSECTIONRECT // }
37
- for (const rect of elementRects.slice(1)) {
38
- if (intersectionRect) {
39
- intersectionRect = intersectionRect.getIntersection(rect);
40
- }
41
- }
42
- return intersectionRect;
43
- }
@@ -1,14 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, 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
- * Loops over the given element's ancestors to find all the scrollable elements.
7
- *
8
- * **Note**: The `document` is always included in the returned array.
9
- *
10
- * @internal
11
- * @param element
12
- * @returns An array of scrollable element's ancestors (including the `document`).
13
- */
14
- export default function getScrollableAncestors(element: HTMLElement): Array<HTMLElement | Document>;
@@ -1,28 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, 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 utils/dom/getscrollableancestors
7
- */
8
- import global from './global';
9
- import findClosestScrollableAncestor from './findclosestscrollableancestor';
10
- /**
11
- * Loops over the given element's ancestors to find all the scrollable elements.
12
- *
13
- * **Note**: The `document` is always included in the returned array.
14
- *
15
- * @internal
16
- * @param element
17
- * @returns An array of scrollable element's ancestors (including the `document`).
18
- */
19
- export default function getScrollableAncestors(element) {
20
- const scrollableAncestors = [];
21
- let scrollableAncestor = findClosestScrollableAncestor(element);
22
- while (scrollableAncestor && scrollableAncestor !== global.document.body) {
23
- scrollableAncestors.push(scrollableAncestor);
24
- scrollableAncestor = findClosestScrollableAncestor(scrollableAncestor);
25
- }
26
- scrollableAncestors.push(global.document);
27
- return scrollableAncestors;
28
- }