@ckeditor/ckeditor5-utils 40.0.0 → 40.2.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/CHANGELOG.md +26 -26
- package/LICENSE.md +3 -3
- package/package.json +1 -1
- package/src/abortabledebounce.d.ts +17 -0
- package/src/abortabledebounce.js +22 -0
- package/src/areconnectedthroughproperties.d.ts +11 -11
- package/src/areconnectedthroughproperties.js +73 -73
- package/src/ckeditorerror.d.ts +123 -123
- package/src/ckeditorerror.js +176 -176
- package/src/collection.d.ts +433 -427
- package/src/collection.js +583 -575
- package/src/comparearrays.d.ts +30 -30
- package/src/comparearrays.js +47 -47
- package/src/config.d.ts +163 -163
- package/src/config.js +163 -162
- package/src/count.d.ts +18 -18
- package/src/count.js +24 -24
- package/src/delay.d.ts +19 -19
- package/src/delay.js +26 -26
- package/src/diff.d.ts +31 -31
- package/src/diff.js +115 -115
- package/src/difftochanges.d.ts +59 -59
- package/src/difftochanges.js +79 -79
- package/src/dom/createelement.d.ts +57 -57
- package/src/dom/createelement.js +40 -40
- package/src/dom/emittermixin.d.ts +142 -142
- package/src/dom/emittermixin.js +239 -239
- package/src/dom/findclosestscrollableancestor.d.ts +11 -11
- package/src/dom/findclosestscrollableancestor.js +31 -31
- package/src/dom/getancestors.d.ts +17 -17
- package/src/dom/getancestors.js +27 -27
- package/src/dom/getborderwidths.d.ts +24 -24
- package/src/dom/getborderwidths.js +24 -24
- package/src/dom/getcommonancestor.d.ts +12 -12
- package/src/dom/getcommonancestor.js +25 -25
- package/src/dom/getdatafromelement.d.ts +14 -14
- package/src/dom/getdatafromelement.js +20 -20
- package/src/dom/getpositionedancestor.d.ts +10 -10
- package/src/dom/getpositionedancestor.js +22 -22
- package/src/dom/global.d.ts +32 -32
- package/src/dom/global.js +35 -35
- package/src/dom/indexof.d.ts +14 -14
- package/src/dom/indexof.js +21 -21
- package/src/dom/insertat.d.ts +15 -15
- package/src/dom/insertat.js +17 -17
- package/src/dom/iscomment.d.ts +11 -11
- package/src/dom/iscomment.js +14 -14
- package/src/dom/isnode.d.ts +11 -11
- package/src/dom/isnode.js +21 -21
- package/src/dom/isrange.d.ts +11 -11
- package/src/dom/isrange.js +13 -13
- package/src/dom/istext.d.ts +11 -11
- package/src/dom/istext.js +13 -13
- package/src/dom/isvalidattributename.d.ts +10 -10
- package/src/dom/isvalidattributename.js +22 -22
- package/src/dom/isvisible.d.ts +18 -18
- package/src/dom/isvisible.js +20 -20
- package/src/dom/iswindow.d.ts +11 -11
- package/src/dom/iswindow.js +22 -22
- package/src/dom/position.d.ts +211 -211
- package/src/dom/position.js +313 -313
- package/src/dom/rect.d.ts +195 -195
- package/src/dom/rect.js +474 -474
- package/src/dom/remove.d.ts +13 -13
- package/src/dom/remove.js +18 -18
- package/src/dom/resizeobserver.d.ts +74 -74
- package/src/dom/resizeobserver.js +126 -126
- package/src/dom/scroll.d.ts +73 -73
- package/src/dom/scroll.js +383 -383
- package/src/dom/setdatainelement.d.ts +14 -14
- package/src/dom/setdatainelement.js +20 -20
- package/src/dom/tounit.d.ts +22 -22
- package/src/dom/tounit.js +16 -16
- package/src/elementreplacer.d.ts +31 -31
- package/src/elementreplacer.js +43 -43
- package/src/emittermixin.d.ts +312 -312
- package/src/emittermixin.js +453 -453
- package/src/env.d.ts +117 -117
- package/src/env.js +122 -122
- package/src/eventinfo.d.ts +58 -58
- package/src/eventinfo.js +26 -26
- package/src/fastdiff.d.ts +112 -112
- package/src/fastdiff.js +248 -248
- package/src/first.d.ts +11 -11
- package/src/first.js +17 -17
- package/src/focustracker.d.ts +75 -75
- package/src/focustracker.js +95 -95
- package/src/index.d.ts +64 -61
- package/src/index.js +63 -60
- package/src/inserttopriorityarray.d.ts +30 -30
- package/src/inserttopriorityarray.js +21 -21
- package/src/isiterable.d.ts +14 -14
- package/src/isiterable.js +16 -16
- package/src/keyboard.d.ts +126 -126
- package/src/keyboard.js +221 -221
- package/src/keystrokehandler.d.ts +87 -87
- package/src/keystrokehandler.js +122 -122
- package/src/language.d.ts +17 -17
- package/src/language.js +19 -19
- package/src/locale.d.ts +120 -120
- package/src/locale.js +76 -76
- package/src/mapsequal.d.ts +15 -15
- package/src/mapsequal.js +27 -27
- package/src/mix.d.ts +85 -85
- package/src/mix.js +50 -50
- package/src/nth.d.ts +16 -16
- package/src/nth.js +24 -24
- package/src/objecttomap.d.ts +23 -23
- package/src/objecttomap.js +27 -27
- package/src/observablemixin.d.ts +560 -560
- package/src/observablemixin.js +580 -580
- package/src/priorities.d.ts +33 -33
- package/src/priorities.js +23 -23
- package/src/retry.d.ts +33 -0
- package/src/retry.js +47 -0
- package/src/splicearray.d.ts +26 -26
- package/src/splicearray.js +40 -40
- package/src/spy.d.ts +21 -21
- package/src/spy.js +22 -22
- package/src/toarray.d.ts +25 -25
- package/src/toarray.js +7 -7
- package/src/tomap.d.ts +19 -19
- package/src/tomap.js +29 -29
- package/src/translation-service.d.ts +168 -168
- package/src/translation-service.js +198 -198
- package/src/uid.d.ts +15 -15
- package/src/uid.js +57 -57
- package/src/unicode.d.ts +54 -54
- package/src/unicode.js +85 -85
- package/src/verifylicense.d.ts +15 -15
- package/src/verifylicense.js +87 -87
- package/src/version.d.ts +10 -10
- package/src/version.js +153 -153
- package/src/wait.d.ts +16 -0
- package/src/wait.js +29 -0
package/src/dom/rect.js
CHANGED
|
@@ -1,474 +1,474 @@
|
|
|
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/rect
|
|
7
|
-
*/
|
|
8
|
-
import isRange from './isrange';
|
|
9
|
-
import isWindow from './iswindow';
|
|
10
|
-
import getBorderWidths from './getborderwidths';
|
|
11
|
-
import isText from './istext';
|
|
12
|
-
import getPositionedAncestor from './getpositionedancestor';
|
|
13
|
-
import global from './global';
|
|
14
|
-
const rectProperties = ['top', 'right', 'bottom', 'left', 'width', 'height'];
|
|
15
|
-
/**
|
|
16
|
-
* A helper class representing a `ClientRect` object, e.g. value returned by
|
|
17
|
-
* the native `object.getBoundingClientRect()` method. Provides a set of methods
|
|
18
|
-
* to manipulate the rect and compare it against other rect instances.
|
|
19
|
-
*/
|
|
20
|
-
export default class Rect {
|
|
21
|
-
/**
|
|
22
|
-
* Creates an instance of rect.
|
|
23
|
-
*
|
|
24
|
-
* ```ts
|
|
25
|
-
* // Rect of an HTMLElement.
|
|
26
|
-
* const rectA = new Rect( document.body );
|
|
27
|
-
*
|
|
28
|
-
* // Rect of a DOM Range.
|
|
29
|
-
* const rectB = new Rect( document.getSelection().getRangeAt( 0 ) );
|
|
30
|
-
*
|
|
31
|
-
* // Rect of a window (web browser viewport).
|
|
32
|
-
* const rectC = new Rect( window );
|
|
33
|
-
*
|
|
34
|
-
* // Rect out of an object.
|
|
35
|
-
* const rectD = new Rect( { top: 0, right: 10, bottom: 10, left: 0, width: 10, height: 10 } );
|
|
36
|
-
*
|
|
37
|
-
* // Rect out of another Rect instance.
|
|
38
|
-
* const rectE = new Rect( rectD );
|
|
39
|
-
*
|
|
40
|
-
* // Rect out of a ClientRect.
|
|
41
|
-
* const rectF = new Rect( document.body.getClientRects().item( 0 ) );
|
|
42
|
-
* ```
|
|
43
|
-
*
|
|
44
|
-
* **Note**: By default a rect of an HTML element includes its CSS borders and scrollbars (if any)
|
|
45
|
-
* ant the rect of a `window` includes scrollbars too. Use {@link #excludeScrollbarsAndBorders}
|
|
46
|
-
* to get the inner part of the rect.
|
|
47
|
-
*
|
|
48
|
-
* @param source A source object to create the rect.
|
|
49
|
-
*/
|
|
50
|
-
constructor(source) {
|
|
51
|
-
const isSourceRange = isRange(source);
|
|
52
|
-
Object.defineProperty(this, '_source', {
|
|
53
|
-
// If the source is a Rect instance, copy it's #_source.
|
|
54
|
-
value: source._source || source,
|
|
55
|
-
writable: true,
|
|
56
|
-
enumerable: false
|
|
57
|
-
});
|
|
58
|
-
if (isDomElement(source) || isSourceRange) {
|
|
59
|
-
// The `Rect` class depends on `getBoundingClientRect` and `getClientRects` DOM methods. If the source
|
|
60
|
-
// of a rect in an HTML element or a DOM range but it does not belong to any rendered DOM tree, these methods
|
|
61
|
-
// will fail to obtain the geometry and the rect instance makes little sense to the features using it.
|
|
62
|
-
// To get rid of this warning make sure the source passed to the constructor is a descendant of `window.document.body`.
|
|
63
|
-
// @if CK_DEBUG // const sourceNode = isSourceRange ? source.startContainer : source;
|
|
64
|
-
// @if CK_DEBUG // if ( !sourceNode.ownerDocument || !sourceNode.ownerDocument.body.contains( sourceNode ) ) {
|
|
65
|
-
// @if CK_DEBUG // console.warn(
|
|
66
|
-
// @if CK_DEBUG // 'rect-source-not-in-dom: The source of this rect does not belong to any rendered DOM tree.',
|
|
67
|
-
// @if CK_DEBUG // { source } );
|
|
68
|
-
// @if CK_DEBUG // }
|
|
69
|
-
if (isSourceRange) {
|
|
70
|
-
const rangeRects = Rect.getDomRangeRects(source);
|
|
71
|
-
copyRectProperties(this, Rect.getBoundingRect(rangeRects));
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
copyRectProperties(this, source.getBoundingClientRect());
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
else if (isWindow(source)) {
|
|
78
|
-
const { innerWidth, innerHeight } = source;
|
|
79
|
-
copyRectProperties(this, {
|
|
80
|
-
top: 0,
|
|
81
|
-
right: innerWidth,
|
|
82
|
-
bottom: innerHeight,
|
|
83
|
-
left: 0,
|
|
84
|
-
width: innerWidth,
|
|
85
|
-
height: innerHeight
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
copyRectProperties(this, source);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Returns a clone of the rect.
|
|
94
|
-
*
|
|
95
|
-
* @returns A cloned rect.
|
|
96
|
-
*/
|
|
97
|
-
clone() {
|
|
98
|
-
return new Rect(this);
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Moves the rect so that its upper–left corner lands in desired `[ x, y ]` location.
|
|
102
|
-
*
|
|
103
|
-
* @param x Desired horizontal location.
|
|
104
|
-
* @param y Desired vertical location.
|
|
105
|
-
* @returns A rect which has been moved.
|
|
106
|
-
*/
|
|
107
|
-
moveTo(x, y) {
|
|
108
|
-
this.top = y;
|
|
109
|
-
this.right = x + this.width;
|
|
110
|
-
this.bottom = y + this.height;
|
|
111
|
-
this.left = x;
|
|
112
|
-
return this;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Moves the rect in–place by a dedicated offset.
|
|
116
|
-
*
|
|
117
|
-
* @param x A horizontal offset.
|
|
118
|
-
* @param y A vertical offset
|
|
119
|
-
* @returns A rect which has been moved.
|
|
120
|
-
*/
|
|
121
|
-
moveBy(x, y) {
|
|
122
|
-
this.top += y;
|
|
123
|
-
this.right += x;
|
|
124
|
-
this.left += x;
|
|
125
|
-
this.bottom += y;
|
|
126
|
-
return this;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Returns a new rect a a result of intersection with another rect.
|
|
130
|
-
*/
|
|
131
|
-
getIntersection(anotherRect) {
|
|
132
|
-
const rect = {
|
|
133
|
-
top: Math.max(this.top, anotherRect.top),
|
|
134
|
-
right: Math.min(this.right, anotherRect.right),
|
|
135
|
-
bottom: Math.min(this.bottom, anotherRect.bottom),
|
|
136
|
-
left: Math.max(this.left, anotherRect.left),
|
|
137
|
-
width: 0,
|
|
138
|
-
height: 0
|
|
139
|
-
};
|
|
140
|
-
rect.width = rect.right - rect.left;
|
|
141
|
-
rect.height = rect.bottom - rect.top;
|
|
142
|
-
if (rect.width < 0 || rect.height < 0) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
const newRect = new Rect(rect);
|
|
147
|
-
newRect._source = this._source;
|
|
148
|
-
return newRect;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Returns the area of intersection with another rect.
|
|
153
|
-
*
|
|
154
|
-
* @returns Area of intersection.
|
|
155
|
-
*/
|
|
156
|
-
getIntersectionArea(anotherRect) {
|
|
157
|
-
const rect = this.getIntersection(anotherRect);
|
|
158
|
-
if (rect) {
|
|
159
|
-
return rect.getArea();
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
return 0;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Returns the area of the rect.
|
|
167
|
-
*/
|
|
168
|
-
getArea() {
|
|
169
|
-
return this.width * this.height;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Returns a new rect, a part of the original rect, which is actually visible to the user and is relative to the,`body`,
|
|
173
|
-
* e.g. an original rect cropped by parent element rects which have `overflow` set in CSS
|
|
174
|
-
* other than `"visible"`.
|
|
175
|
-
*
|
|
176
|
-
* If there's no such visible rect, which is when the rect is limited by one or many of
|
|
177
|
-
* the ancestors, `null` is returned.
|
|
178
|
-
*
|
|
179
|
-
* **Note**: This method does not consider the boundaries of the viewport (window).
|
|
180
|
-
* To get a rect cropped by all ancestors and the viewport, use an intersection such as:
|
|
181
|
-
*
|
|
182
|
-
* ```ts
|
|
183
|
-
* const visibleInViewportRect = new Rect( window ).getIntersection( new Rect( source ).getVisible() );
|
|
184
|
-
* ```
|
|
185
|
-
*
|
|
186
|
-
* @returns A visible rect instance or `null`, if there's none.
|
|
187
|
-
*/
|
|
188
|
-
getVisible() {
|
|
189
|
-
const source = this._source;
|
|
190
|
-
let visibleRect = this.clone();
|
|
191
|
-
// There's no ancestor to crop <body> with the overflow.
|
|
192
|
-
if (isBody(source)) {
|
|
193
|
-
return visibleRect;
|
|
194
|
-
}
|
|
195
|
-
let child = source;
|
|
196
|
-
let parent = source.parentNode || source.commonAncestorContainer;
|
|
197
|
-
let absolutelyPositionedChildElement;
|
|
198
|
-
// Check the ancestors all the way up to the <body>.
|
|
199
|
-
while (parent && !isBody(parent)) {
|
|
200
|
-
const isParentOverflowVisible = getElementOverflow(parent) === 'visible';
|
|
201
|
-
if (child instanceof HTMLElement && getElementPosition(child) === 'absolute') {
|
|
202
|
-
absolutelyPositionedChildElement = child;
|
|
203
|
-
}
|
|
204
|
-
const parentElementPosition = getElementPosition(parent);
|
|
205
|
-
// The child will be cropped only if it has `position: absolute` and the parent has `position: relative` + some overflow.
|
|
206
|
-
// Otherwise there's no chance of visual clipping and the parent can be skipped
|
|
207
|
-
// https://github.com/ckeditor/ckeditor5/issues/14107.
|
|
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')) {
|
|
241
|
-
child = parent;
|
|
242
|
-
parent = parent.parentNode;
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
const parentRect = new Rect(parent);
|
|
246
|
-
const intersectionRect = visibleRect.getIntersection(parentRect);
|
|
247
|
-
if (intersectionRect) {
|
|
248
|
-
if (intersectionRect.getArea() < visibleRect.getArea()) {
|
|
249
|
-
// Reduce the visible rect to the intersection.
|
|
250
|
-
visibleRect = intersectionRect;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
// There's no intersection, the rect is completely invisible.
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
child = parent;
|
|
258
|
-
parent = parent.parentNode;
|
|
259
|
-
}
|
|
260
|
-
return visibleRect;
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Checks if all property values ({@link #top}, {@link #left}, {@link #right},
|
|
264
|
-
* {@link #bottom}, {@link #width} and {@link #height}) are the equal in both rect
|
|
265
|
-
* instances.
|
|
266
|
-
*
|
|
267
|
-
* @param anotherRect A rect instance to compare with.
|
|
268
|
-
* @returns `true` when Rects are equal. `false` otherwise.
|
|
269
|
-
*/
|
|
270
|
-
isEqual(anotherRect) {
|
|
271
|
-
for (const prop of rectProperties) {
|
|
272
|
-
if (this[prop] !== anotherRect[prop]) {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Checks whether a rect fully contains another rect instance.
|
|
280
|
-
*
|
|
281
|
-
* @param anotherRect
|
|
282
|
-
* @returns `true` if contains, `false` otherwise.
|
|
283
|
-
*/
|
|
284
|
-
contains(anotherRect) {
|
|
285
|
-
const intersectRect = this.getIntersection(anotherRect);
|
|
286
|
-
return !!(intersectRect && intersectRect.isEqual(anotherRect));
|
|
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
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Excludes scrollbars and CSS borders from the rect.
|
|
304
|
-
*
|
|
305
|
-
* * Borders are removed when {@link #_source} is an HTML element.
|
|
306
|
-
* * Scrollbars are excluded from HTML elements and the `window`.
|
|
307
|
-
*
|
|
308
|
-
* @returns A rect which has been updated.
|
|
309
|
-
*/
|
|
310
|
-
excludeScrollbarsAndBorders() {
|
|
311
|
-
const source = this._source;
|
|
312
|
-
let scrollBarWidth, scrollBarHeight, direction;
|
|
313
|
-
if (isWindow(source)) {
|
|
314
|
-
scrollBarWidth = source.innerWidth - source.document.documentElement.clientWidth;
|
|
315
|
-
scrollBarHeight = source.innerHeight - source.document.documentElement.clientHeight;
|
|
316
|
-
direction = source.getComputedStyle(source.document.documentElement).direction;
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
const borderWidths = getBorderWidths(source);
|
|
320
|
-
scrollBarWidth = source.offsetWidth - source.clientWidth - borderWidths.left - borderWidths.right;
|
|
321
|
-
scrollBarHeight = source.offsetHeight - source.clientHeight - borderWidths.top - borderWidths.bottom;
|
|
322
|
-
direction = source.ownerDocument.defaultView.getComputedStyle(source).direction;
|
|
323
|
-
this.left += borderWidths.left;
|
|
324
|
-
this.top += borderWidths.top;
|
|
325
|
-
this.right -= borderWidths.right;
|
|
326
|
-
this.bottom -= borderWidths.bottom;
|
|
327
|
-
this.width = this.right - this.left;
|
|
328
|
-
this.height = this.bottom - this.top;
|
|
329
|
-
}
|
|
330
|
-
this.width -= scrollBarWidth;
|
|
331
|
-
if (direction === 'ltr') {
|
|
332
|
-
this.right -= scrollBarWidth;
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
this.left += scrollBarWidth;
|
|
336
|
-
}
|
|
337
|
-
this.height -= scrollBarHeight;
|
|
338
|
-
this.bottom -= scrollBarHeight;
|
|
339
|
-
return this;
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Returns an array of rects of the given native DOM Range.
|
|
343
|
-
*
|
|
344
|
-
* @param range A native DOM range.
|
|
345
|
-
* @returns DOM Range rects.
|
|
346
|
-
*/
|
|
347
|
-
static getDomRangeRects(range) {
|
|
348
|
-
const rects = [];
|
|
349
|
-
// Safari does not iterate over ClientRectList using for...of loop.
|
|
350
|
-
const clientRects = Array.from(range.getClientRects());
|
|
351
|
-
if (clientRects.length) {
|
|
352
|
-
for (const rect of clientRects) {
|
|
353
|
-
rects.push(new Rect(rect));
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
// If there's no client rects for the Range, use parent container's bounding rect
|
|
357
|
-
// instead and adjust rect's width to simulate the actual geometry of such range.
|
|
358
|
-
// https://github.com/ckeditor/ckeditor5-utils/issues/153
|
|
359
|
-
// https://github.com/ckeditor/ckeditor5-ui/issues/317
|
|
360
|
-
else {
|
|
361
|
-
let startContainer = range.startContainer;
|
|
362
|
-
if (isText(startContainer)) {
|
|
363
|
-
startContainer = startContainer.parentNode;
|
|
364
|
-
}
|
|
365
|
-
const rect = new Rect(startContainer.getBoundingClientRect());
|
|
366
|
-
rect.right = rect.left;
|
|
367
|
-
rect.width = 0;
|
|
368
|
-
rects.push(rect);
|
|
369
|
-
}
|
|
370
|
-
return rects;
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Returns a bounding rectangle that contains all the given `rects`.
|
|
374
|
-
*
|
|
375
|
-
* @param rects A list of rectangles that should be contained in the result rectangle.
|
|
376
|
-
* @returns Bounding rectangle or `null` if no `rects` were given.
|
|
377
|
-
*/
|
|
378
|
-
static getBoundingRect(rects) {
|
|
379
|
-
const boundingRectData = {
|
|
380
|
-
left: Number.POSITIVE_INFINITY,
|
|
381
|
-
top: Number.POSITIVE_INFINITY,
|
|
382
|
-
right: Number.NEGATIVE_INFINITY,
|
|
383
|
-
bottom: Number.NEGATIVE_INFINITY,
|
|
384
|
-
width: 0,
|
|
385
|
-
height: 0
|
|
386
|
-
};
|
|
387
|
-
let rectangleCount = 0;
|
|
388
|
-
for (const rect of rects) {
|
|
389
|
-
rectangleCount++;
|
|
390
|
-
boundingRectData.left = Math.min(boundingRectData.left, rect.left);
|
|
391
|
-
boundingRectData.top = Math.min(boundingRectData.top, rect.top);
|
|
392
|
-
boundingRectData.right = Math.max(boundingRectData.right, rect.right);
|
|
393
|
-
boundingRectData.bottom = Math.max(boundingRectData.bottom, rect.bottom);
|
|
394
|
-
}
|
|
395
|
-
if (rectangleCount == 0) {
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
boundingRectData.width = boundingRectData.right - boundingRectData.left;
|
|
399
|
-
boundingRectData.height = boundingRectData.bottom - boundingRectData.top;
|
|
400
|
-
return new Rect(boundingRectData);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Acquires all the rect properties from the passed source.
|
|
405
|
-
*/
|
|
406
|
-
function copyRectProperties(rect, source) {
|
|
407
|
-
for (const p of rectProperties) {
|
|
408
|
-
rect[p] = source[p];
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Checks if provided object is a <body> HTML element.
|
|
413
|
-
*/
|
|
414
|
-
function isBody(value) {
|
|
415
|
-
if (!isDomElement(value)) {
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
return value === value.ownerDocument.body;
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Checks if provided object "looks like" a DOM Element and has API required by `Rect` class.
|
|
422
|
-
*/
|
|
423
|
-
function isDomElement(value) {
|
|
424
|
-
// Note: earlier we used `isElement()` from lodash library, however that function is less performant because
|
|
425
|
-
// it makes complicated checks to make sure that given value is a DOM element.
|
|
426
|
-
return value !== null && typeof value === 'object' && value.nodeType === 1 && typeof value.getBoundingClientRect === 'function';
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Returns the value of the `position` style of an `HTMLElement`.
|
|
430
|
-
*/
|
|
431
|
-
function getElementPosition(element) {
|
|
432
|
-
return element instanceof HTMLElement ? element.ownerDocument.defaultView.getComputedStyle(element).position : 'static';
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Returns the value of the `overflow` style of an `HTMLElement` or a `Range`.
|
|
436
|
-
*/
|
|
437
|
-
function getElementOverflow(element) {
|
|
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);
|
|
474
|
-
}
|
|
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/rect
|
|
7
|
+
*/
|
|
8
|
+
import isRange from './isrange';
|
|
9
|
+
import isWindow from './iswindow';
|
|
10
|
+
import getBorderWidths from './getborderwidths';
|
|
11
|
+
import isText from './istext';
|
|
12
|
+
import getPositionedAncestor from './getpositionedancestor';
|
|
13
|
+
import global from './global';
|
|
14
|
+
const rectProperties = ['top', 'right', 'bottom', 'left', 'width', 'height'];
|
|
15
|
+
/**
|
|
16
|
+
* A helper class representing a `ClientRect` object, e.g. value returned by
|
|
17
|
+
* the native `object.getBoundingClientRect()` method. Provides a set of methods
|
|
18
|
+
* to manipulate the rect and compare it against other rect instances.
|
|
19
|
+
*/
|
|
20
|
+
export default class Rect {
|
|
21
|
+
/**
|
|
22
|
+
* Creates an instance of rect.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* // Rect of an HTMLElement.
|
|
26
|
+
* const rectA = new Rect( document.body );
|
|
27
|
+
*
|
|
28
|
+
* // Rect of a DOM Range.
|
|
29
|
+
* const rectB = new Rect( document.getSelection().getRangeAt( 0 ) );
|
|
30
|
+
*
|
|
31
|
+
* // Rect of a window (web browser viewport).
|
|
32
|
+
* const rectC = new Rect( window );
|
|
33
|
+
*
|
|
34
|
+
* // Rect out of an object.
|
|
35
|
+
* const rectD = new Rect( { top: 0, right: 10, bottom: 10, left: 0, width: 10, height: 10 } );
|
|
36
|
+
*
|
|
37
|
+
* // Rect out of another Rect instance.
|
|
38
|
+
* const rectE = new Rect( rectD );
|
|
39
|
+
*
|
|
40
|
+
* // Rect out of a ClientRect.
|
|
41
|
+
* const rectF = new Rect( document.body.getClientRects().item( 0 ) );
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* **Note**: By default a rect of an HTML element includes its CSS borders and scrollbars (if any)
|
|
45
|
+
* ant the rect of a `window` includes scrollbars too. Use {@link #excludeScrollbarsAndBorders}
|
|
46
|
+
* to get the inner part of the rect.
|
|
47
|
+
*
|
|
48
|
+
* @param source A source object to create the rect.
|
|
49
|
+
*/
|
|
50
|
+
constructor(source) {
|
|
51
|
+
const isSourceRange = isRange(source);
|
|
52
|
+
Object.defineProperty(this, '_source', {
|
|
53
|
+
// If the source is a Rect instance, copy it's #_source.
|
|
54
|
+
value: source._source || source,
|
|
55
|
+
writable: true,
|
|
56
|
+
enumerable: false
|
|
57
|
+
});
|
|
58
|
+
if (isDomElement(source) || isSourceRange) {
|
|
59
|
+
// The `Rect` class depends on `getBoundingClientRect` and `getClientRects` DOM methods. If the source
|
|
60
|
+
// of a rect in an HTML element or a DOM range but it does not belong to any rendered DOM tree, these methods
|
|
61
|
+
// will fail to obtain the geometry and the rect instance makes little sense to the features using it.
|
|
62
|
+
// To get rid of this warning make sure the source passed to the constructor is a descendant of `window.document.body`.
|
|
63
|
+
// @if CK_DEBUG // const sourceNode = isSourceRange ? source.startContainer : source;
|
|
64
|
+
// @if CK_DEBUG // if ( !sourceNode.ownerDocument || !sourceNode.ownerDocument.body.contains( sourceNode ) ) {
|
|
65
|
+
// @if CK_DEBUG // console.warn(
|
|
66
|
+
// @if CK_DEBUG // 'rect-source-not-in-dom: The source of this rect does not belong to any rendered DOM tree.',
|
|
67
|
+
// @if CK_DEBUG // { source } );
|
|
68
|
+
// @if CK_DEBUG // }
|
|
69
|
+
if (isSourceRange) {
|
|
70
|
+
const rangeRects = Rect.getDomRangeRects(source);
|
|
71
|
+
copyRectProperties(this, Rect.getBoundingRect(rangeRects));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
copyRectProperties(this, source.getBoundingClientRect());
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (isWindow(source)) {
|
|
78
|
+
const { innerWidth, innerHeight } = source;
|
|
79
|
+
copyRectProperties(this, {
|
|
80
|
+
top: 0,
|
|
81
|
+
right: innerWidth,
|
|
82
|
+
bottom: innerHeight,
|
|
83
|
+
left: 0,
|
|
84
|
+
width: innerWidth,
|
|
85
|
+
height: innerHeight
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
copyRectProperties(this, source);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Returns a clone of the rect.
|
|
94
|
+
*
|
|
95
|
+
* @returns A cloned rect.
|
|
96
|
+
*/
|
|
97
|
+
clone() {
|
|
98
|
+
return new Rect(this);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Moves the rect so that its upper–left corner lands in desired `[ x, y ]` location.
|
|
102
|
+
*
|
|
103
|
+
* @param x Desired horizontal location.
|
|
104
|
+
* @param y Desired vertical location.
|
|
105
|
+
* @returns A rect which has been moved.
|
|
106
|
+
*/
|
|
107
|
+
moveTo(x, y) {
|
|
108
|
+
this.top = y;
|
|
109
|
+
this.right = x + this.width;
|
|
110
|
+
this.bottom = y + this.height;
|
|
111
|
+
this.left = x;
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Moves the rect in–place by a dedicated offset.
|
|
116
|
+
*
|
|
117
|
+
* @param x A horizontal offset.
|
|
118
|
+
* @param y A vertical offset
|
|
119
|
+
* @returns A rect which has been moved.
|
|
120
|
+
*/
|
|
121
|
+
moveBy(x, y) {
|
|
122
|
+
this.top += y;
|
|
123
|
+
this.right += x;
|
|
124
|
+
this.left += x;
|
|
125
|
+
this.bottom += y;
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Returns a new rect a a result of intersection with another rect.
|
|
130
|
+
*/
|
|
131
|
+
getIntersection(anotherRect) {
|
|
132
|
+
const rect = {
|
|
133
|
+
top: Math.max(this.top, anotherRect.top),
|
|
134
|
+
right: Math.min(this.right, anotherRect.right),
|
|
135
|
+
bottom: Math.min(this.bottom, anotherRect.bottom),
|
|
136
|
+
left: Math.max(this.left, anotherRect.left),
|
|
137
|
+
width: 0,
|
|
138
|
+
height: 0
|
|
139
|
+
};
|
|
140
|
+
rect.width = rect.right - rect.left;
|
|
141
|
+
rect.height = rect.bottom - rect.top;
|
|
142
|
+
if (rect.width < 0 || rect.height < 0) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const newRect = new Rect(rect);
|
|
147
|
+
newRect._source = this._source;
|
|
148
|
+
return newRect;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Returns the area of intersection with another rect.
|
|
153
|
+
*
|
|
154
|
+
* @returns Area of intersection.
|
|
155
|
+
*/
|
|
156
|
+
getIntersectionArea(anotherRect) {
|
|
157
|
+
const rect = this.getIntersection(anotherRect);
|
|
158
|
+
if (rect) {
|
|
159
|
+
return rect.getArea();
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Returns the area of the rect.
|
|
167
|
+
*/
|
|
168
|
+
getArea() {
|
|
169
|
+
return this.width * this.height;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Returns a new rect, a part of the original rect, which is actually visible to the user and is relative to the,`body`,
|
|
173
|
+
* e.g. an original rect cropped by parent element rects which have `overflow` set in CSS
|
|
174
|
+
* other than `"visible"`.
|
|
175
|
+
*
|
|
176
|
+
* If there's no such visible rect, which is when the rect is limited by one or many of
|
|
177
|
+
* the ancestors, `null` is returned.
|
|
178
|
+
*
|
|
179
|
+
* **Note**: This method does not consider the boundaries of the viewport (window).
|
|
180
|
+
* To get a rect cropped by all ancestors and the viewport, use an intersection such as:
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* const visibleInViewportRect = new Rect( window ).getIntersection( new Rect( source ).getVisible() );
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* @returns A visible rect instance or `null`, if there's none.
|
|
187
|
+
*/
|
|
188
|
+
getVisible() {
|
|
189
|
+
const source = this._source;
|
|
190
|
+
let visibleRect = this.clone();
|
|
191
|
+
// There's no ancestor to crop <body> with the overflow.
|
|
192
|
+
if (isBody(source)) {
|
|
193
|
+
return visibleRect;
|
|
194
|
+
}
|
|
195
|
+
let child = source;
|
|
196
|
+
let parent = source.parentNode || source.commonAncestorContainer;
|
|
197
|
+
let absolutelyPositionedChildElement;
|
|
198
|
+
// Check the ancestors all the way up to the <body>.
|
|
199
|
+
while (parent && !isBody(parent)) {
|
|
200
|
+
const isParentOverflowVisible = getElementOverflow(parent) === 'visible';
|
|
201
|
+
if (child instanceof HTMLElement && getElementPosition(child) === 'absolute') {
|
|
202
|
+
absolutelyPositionedChildElement = child;
|
|
203
|
+
}
|
|
204
|
+
const parentElementPosition = getElementPosition(parent);
|
|
205
|
+
// The child will be cropped only if it has `position: absolute` and the parent has `position: relative` + some overflow.
|
|
206
|
+
// Otherwise there's no chance of visual clipping and the parent can be skipped
|
|
207
|
+
// https://github.com/ckeditor/ckeditor5/issues/14107.
|
|
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')) {
|
|
241
|
+
child = parent;
|
|
242
|
+
parent = parent.parentNode;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const parentRect = new Rect(parent);
|
|
246
|
+
const intersectionRect = visibleRect.getIntersection(parentRect);
|
|
247
|
+
if (intersectionRect) {
|
|
248
|
+
if (intersectionRect.getArea() < visibleRect.getArea()) {
|
|
249
|
+
// Reduce the visible rect to the intersection.
|
|
250
|
+
visibleRect = intersectionRect;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// There's no intersection, the rect is completely invisible.
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
child = parent;
|
|
258
|
+
parent = parent.parentNode;
|
|
259
|
+
}
|
|
260
|
+
return visibleRect;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Checks if all property values ({@link #top}, {@link #left}, {@link #right},
|
|
264
|
+
* {@link #bottom}, {@link #width} and {@link #height}) are the equal in both rect
|
|
265
|
+
* instances.
|
|
266
|
+
*
|
|
267
|
+
* @param anotherRect A rect instance to compare with.
|
|
268
|
+
* @returns `true` when Rects are equal. `false` otherwise.
|
|
269
|
+
*/
|
|
270
|
+
isEqual(anotherRect) {
|
|
271
|
+
for (const prop of rectProperties) {
|
|
272
|
+
if (this[prop] !== anotherRect[prop]) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Checks whether a rect fully contains another rect instance.
|
|
280
|
+
*
|
|
281
|
+
* @param anotherRect
|
|
282
|
+
* @returns `true` if contains, `false` otherwise.
|
|
283
|
+
*/
|
|
284
|
+
contains(anotherRect) {
|
|
285
|
+
const intersectRect = this.getIntersection(anotherRect);
|
|
286
|
+
return !!(intersectRect && intersectRect.isEqual(anotherRect));
|
|
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
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Excludes scrollbars and CSS borders from the rect.
|
|
304
|
+
*
|
|
305
|
+
* * Borders are removed when {@link #_source} is an HTML element.
|
|
306
|
+
* * Scrollbars are excluded from HTML elements and the `window`.
|
|
307
|
+
*
|
|
308
|
+
* @returns A rect which has been updated.
|
|
309
|
+
*/
|
|
310
|
+
excludeScrollbarsAndBorders() {
|
|
311
|
+
const source = this._source;
|
|
312
|
+
let scrollBarWidth, scrollBarHeight, direction;
|
|
313
|
+
if (isWindow(source)) {
|
|
314
|
+
scrollBarWidth = source.innerWidth - source.document.documentElement.clientWidth;
|
|
315
|
+
scrollBarHeight = source.innerHeight - source.document.documentElement.clientHeight;
|
|
316
|
+
direction = source.getComputedStyle(source.document.documentElement).direction;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
const borderWidths = getBorderWidths(source);
|
|
320
|
+
scrollBarWidth = source.offsetWidth - source.clientWidth - borderWidths.left - borderWidths.right;
|
|
321
|
+
scrollBarHeight = source.offsetHeight - source.clientHeight - borderWidths.top - borderWidths.bottom;
|
|
322
|
+
direction = source.ownerDocument.defaultView.getComputedStyle(source).direction;
|
|
323
|
+
this.left += borderWidths.left;
|
|
324
|
+
this.top += borderWidths.top;
|
|
325
|
+
this.right -= borderWidths.right;
|
|
326
|
+
this.bottom -= borderWidths.bottom;
|
|
327
|
+
this.width = this.right - this.left;
|
|
328
|
+
this.height = this.bottom - this.top;
|
|
329
|
+
}
|
|
330
|
+
this.width -= scrollBarWidth;
|
|
331
|
+
if (direction === 'ltr') {
|
|
332
|
+
this.right -= scrollBarWidth;
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
this.left += scrollBarWidth;
|
|
336
|
+
}
|
|
337
|
+
this.height -= scrollBarHeight;
|
|
338
|
+
this.bottom -= scrollBarHeight;
|
|
339
|
+
return this;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Returns an array of rects of the given native DOM Range.
|
|
343
|
+
*
|
|
344
|
+
* @param range A native DOM range.
|
|
345
|
+
* @returns DOM Range rects.
|
|
346
|
+
*/
|
|
347
|
+
static getDomRangeRects(range) {
|
|
348
|
+
const rects = [];
|
|
349
|
+
// Safari does not iterate over ClientRectList using for...of loop.
|
|
350
|
+
const clientRects = Array.from(range.getClientRects());
|
|
351
|
+
if (clientRects.length) {
|
|
352
|
+
for (const rect of clientRects) {
|
|
353
|
+
rects.push(new Rect(rect));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// If there's no client rects for the Range, use parent container's bounding rect
|
|
357
|
+
// instead and adjust rect's width to simulate the actual geometry of such range.
|
|
358
|
+
// https://github.com/ckeditor/ckeditor5-utils/issues/153
|
|
359
|
+
// https://github.com/ckeditor/ckeditor5-ui/issues/317
|
|
360
|
+
else {
|
|
361
|
+
let startContainer = range.startContainer;
|
|
362
|
+
if (isText(startContainer)) {
|
|
363
|
+
startContainer = startContainer.parentNode;
|
|
364
|
+
}
|
|
365
|
+
const rect = new Rect(startContainer.getBoundingClientRect());
|
|
366
|
+
rect.right = rect.left;
|
|
367
|
+
rect.width = 0;
|
|
368
|
+
rects.push(rect);
|
|
369
|
+
}
|
|
370
|
+
return rects;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Returns a bounding rectangle that contains all the given `rects`.
|
|
374
|
+
*
|
|
375
|
+
* @param rects A list of rectangles that should be contained in the result rectangle.
|
|
376
|
+
* @returns Bounding rectangle or `null` if no `rects` were given.
|
|
377
|
+
*/
|
|
378
|
+
static getBoundingRect(rects) {
|
|
379
|
+
const boundingRectData = {
|
|
380
|
+
left: Number.POSITIVE_INFINITY,
|
|
381
|
+
top: Number.POSITIVE_INFINITY,
|
|
382
|
+
right: Number.NEGATIVE_INFINITY,
|
|
383
|
+
bottom: Number.NEGATIVE_INFINITY,
|
|
384
|
+
width: 0,
|
|
385
|
+
height: 0
|
|
386
|
+
};
|
|
387
|
+
let rectangleCount = 0;
|
|
388
|
+
for (const rect of rects) {
|
|
389
|
+
rectangleCount++;
|
|
390
|
+
boundingRectData.left = Math.min(boundingRectData.left, rect.left);
|
|
391
|
+
boundingRectData.top = Math.min(boundingRectData.top, rect.top);
|
|
392
|
+
boundingRectData.right = Math.max(boundingRectData.right, rect.right);
|
|
393
|
+
boundingRectData.bottom = Math.max(boundingRectData.bottom, rect.bottom);
|
|
394
|
+
}
|
|
395
|
+
if (rectangleCount == 0) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
boundingRectData.width = boundingRectData.right - boundingRectData.left;
|
|
399
|
+
boundingRectData.height = boundingRectData.bottom - boundingRectData.top;
|
|
400
|
+
return new Rect(boundingRectData);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Acquires all the rect properties from the passed source.
|
|
405
|
+
*/
|
|
406
|
+
function copyRectProperties(rect, source) {
|
|
407
|
+
for (const p of rectProperties) {
|
|
408
|
+
rect[p] = source[p];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Checks if provided object is a <body> HTML element.
|
|
413
|
+
*/
|
|
414
|
+
function isBody(value) {
|
|
415
|
+
if (!isDomElement(value)) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
return value === value.ownerDocument.body;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Checks if provided object "looks like" a DOM Element and has API required by `Rect` class.
|
|
422
|
+
*/
|
|
423
|
+
function isDomElement(value) {
|
|
424
|
+
// Note: earlier we used `isElement()` from lodash library, however that function is less performant because
|
|
425
|
+
// it makes complicated checks to make sure that given value is a DOM element.
|
|
426
|
+
return value !== null && typeof value === 'object' && value.nodeType === 1 && typeof value.getBoundingClientRect === 'function';
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Returns the value of the `position` style of an `HTMLElement`.
|
|
430
|
+
*/
|
|
431
|
+
function getElementPosition(element) {
|
|
432
|
+
return element instanceof HTMLElement ? element.ownerDocument.defaultView.getComputedStyle(element).position : 'static';
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Returns the value of the `overflow` style of an `HTMLElement` or a `Range`.
|
|
436
|
+
*/
|
|
437
|
+
function getElementOverflow(element) {
|
|
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);
|
|
474
|
+
}
|