-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
dom.js
176 lines (154 loc) · 4.9 KB
/
dom.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
const BLOCK_SELECTOR = '.block-editor-block-list__block';
const APPENDER_SELECTOR = '.block-list-appender';
const BLOCK_APPENDER_CLASS = '.block-editor-button-block-appender';
/**
* Returns true if two elements are contained within the same block.
*
* @param {Element} a First element.
* @param {Element} b Second element.
*
* @return {boolean} Whether elements are in the same block.
*/
export function isInSameBlock( a, b ) {
return a.closest( BLOCK_SELECTOR ) === b.closest( BLOCK_SELECTOR );
}
/**
* Returns true if an element is considered part of the block and not its inner
* blocks or appender.
*
* @param {Element} blockElement Block container element.
* @param {Element} element Element.
*
* @return {boolean} Whether an element is considered part of the block and not
* its inner blocks or appender.
*/
export function isInsideRootBlock( blockElement, element ) {
const parentBlock = element.closest(
[ BLOCK_SELECTOR, APPENDER_SELECTOR, BLOCK_APPENDER_CLASS ].join( ',' )
);
return parentBlock === blockElement;
}
/**
* Finds the block client ID given any DOM node inside the block.
*
* @param {Node?} node DOM node.
*
* @return {string|undefined} Client ID or undefined if the node is not part of
* a block.
*/
export function getBlockClientId( node ) {
while ( node && node.nodeType !== node.ELEMENT_NODE ) {
node = node.parentNode;
}
if ( ! node ) {
return;
}
const elementNode = /** @type {Element} */ ( node );
const blockNode = elementNode.closest( BLOCK_SELECTOR );
if ( ! blockNode ) {
return;
}
return blockNode.id.slice( 'block-'.length );
}
/**
* Calculates the union of two rectangles.
*
* @param {DOMRect} rect1 First rectangle.
* @param {DOMRect} rect2 Second rectangle.
* @return {DOMRect} Union of the two rectangles.
*/
export function rectUnion( rect1, rect2 ) {
const left = Math.min( rect1.left, rect2.left );
const right = Math.max( rect1.right, rect2.right );
const bottom = Math.max( rect1.bottom, rect2.bottom );
const top = Math.min( rect1.top, rect2.top );
return new window.DOMRectReadOnly( left, top, right - left, bottom - top );
}
/**
* Returns whether an element is visible.
*
* @param {Element} element Element.
* @return {boolean} Whether the element is visible.
*/
function isElementVisible( element ) {
const viewport = element.ownerDocument.defaultView;
if ( ! viewport ) {
return false;
}
// Check for <VisuallyHidden> component.
if ( element.classList.contains( 'components-visually-hidden' ) ) {
return false;
}
const bounds = element.getBoundingClientRect();
if ( bounds.width === 0 || bounds.height === 0 ) {
return false;
}
// Older browsers, e.g. Safari < 17.4 may not support the `checkVisibility` method.
if ( element.checkVisibility ) {
return element.checkVisibility?.( {
opacityProperty: true,
contentVisibilityAuto: true,
visibilityProperty: true,
} );
}
const style = viewport.getComputedStyle( element );
if (
style.display === 'none' ||
style.visibility === 'hidden' ||
style.opacity === '0'
) {
return false;
}
return true;
}
/**
* Returns the rect of the element including all visible nested elements.
*
* Visible nested elements, including elements that overflow the parent, are
* taken into account.
*
* This function is useful for calculating the visible area of a block that
* contains nested elements that overflow the block, e.g. the Navigation block,
* which can contain overflowing Submenu blocks.
*
* The returned rect represents the full extent of the element and its visible
* children, which may extend beyond the viewport.
*
* @param {Element} element Element.
* @return {DOMRect} Bounding client rect of the element and its visible children.
*/
export function getVisibleElementBounds( element ) {
const viewport = element.ownerDocument.defaultView;
if ( ! viewport ) {
return new window.DOMRectReadOnly();
}
let bounds = element.getBoundingClientRect();
const stack = [ element ];
let currentElement;
while ( ( currentElement = stack.pop() ) ) {
for ( const child of currentElement.children ) {
if ( isElementVisible( child ) ) {
const childBounds = child.getBoundingClientRect();
bounds = rectUnion( bounds, childBounds );
stack.push( child );
}
}
}
/*
* Take into account the outer horizontal limits of the container in which
* an element is supposed to be "visible". For example, if an element is
* positioned -10px to the left of the window x value (0), this function
* discounts the negative overhang because it's not visible and therefore
* not to be counted in the visibility calculations. Top and bottom values
* are not accounted for to accommodate vertical scroll.
*/
const left = Math.max( bounds.left, 0 );
const right = Math.min( bounds.right, viewport.innerWidth );
bounds = new window.DOMRectReadOnly(
left,
bounds.top,
right - left,
bounds.height
);
return bounds;
}