-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathDOMElement.ts
208 lines (181 loc) · 6.09 KB
/
DOMElement.ts
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import { resizeManager, ResizeManager, ResizeManagerEntry } from '../../utils/ResizeManager'
import { throwError } from '../../utils/utils'
/**
* Defines a rectangular coordinates object
*/
export interface RectCoords {
/** top position */
top: number
/** right position */
right: number
/** bottom position */
bottom: number
/** left position */
left: number
}
/**
* Defines a size object
*/
export interface RectSize {
/** width of the rectangle */
width: number
/** height of the rectangle */
height: number
}
/**
* Defines a rectangular bounding box object
*/
export interface RectBBox extends RectSize {
/** top position of the bounding box */
top: number
/** left position of the bounding box */
left: number
}
/**
* Defines a DOM position object
*/
export interface DOMPosition {
/** X position */
x: number
/** Y position */
y: number
}
/**
* Defines a complete DOM Element bounding rect object, similar to a {@link DOMRect}
*/
export interface DOMElementBoundingRect extends RectCoords, RectBBox, DOMPosition {}
/**
* Parameters used to create a {@link DOMElement}
*/
export interface DOMElementParams {
/** {@link HTMLElement} or string representing an {@link HTMLElement} selector of the element the resize observer should track */
element?: string | Element
/** Order in which the {@link resizeManager} callback is executed */
priority?: ResizeManagerEntry['priority']
/** Callback to tun when the {@link DOMElement#element | element} size changed */
onSizeChanged?: (boundingRect: DOMElementBoundingRect | null) => void | null
/** Callback to tun when the {@link DOMElement#element | element} position changed */
onPositionChanged?: (boundingRect: DOMElementBoundingRect | null) => void | null
}
/**
* Used to track a DOM Element size and position by using a resize observer provided by {@link ResizeManager}.<br>
* Execute callbacks when the bounding rectangle of the DOM Element changes, which means when its size and/or position change.
*/
export class DOMElement {
/** The HTML element to track */
element: HTMLElement
/** Priority at which this element {@link onSizeChanged} function must be called */
priority: ResizeManagerEntry['priority']
/** Flag indicating whether the timeout is still running and we should avoid a new computation */
isResizing: boolean
/** Callback to run whenever the {@link element} size changed */
onSizeChanged: (boundingRect: DOMElementBoundingRect | null) => void | null
/** Callback to run whenever the {@link element} position changed */
onPositionChanged: (boundingRect: DOMElementBoundingRect | null) => void | null
/** The {@link ResizeManager} used, basically a wrapper around a {@link ResizeObserver} */
resizeManager: ResizeManager
/** Current {@link element} bounding rectangle */
_boundingRect: DOMElementBoundingRect
/**
* DOMElement constructor
* @param parameters - {@link DOMElementParams | parameters} used to create our DOMElement
*/
constructor(
{
element = document.body,
priority = 1,
onSizeChanged = (boundingRect = null) => {
/* allow empty callback */
},
onPositionChanged = (boundingRect = null) => {
/* allow empty callback */
},
} = {} as DOMElementParams
) {
if (typeof element === 'string') {
this.element = document.querySelector(element)
if (!this.element) {
const notFoundEl = typeof element === 'string' ? `'${element}' selector` : `${element} HTMLElement`
throwError(`DOMElement: corresponding ${notFoundEl} not found.`)
}
} else {
this.element = element as HTMLElement
}
this.priority = priority
this.isResizing = false
this.onSizeChanged = onSizeChanged
this.onPositionChanged = onPositionChanged
this.resizeManager = resizeManager
this.resizeManager.observe({
element: this.element,
priority: this.priority,
callback: () => {
this.setSize()
},
})
// set size right away on init
this.setSize()
}
/**
* Check whether 2 bounding rectangles are equals
* @param rect1 - first bounding rectangle
* @param rect2 - second bounding rectangle
* @returns - whether the rectangles are equals or not
*/
compareBoundingRect(rect1: DOMRect | DOMElementBoundingRect, rect2: DOMRect | DOMElementBoundingRect): boolean {
return !['x', 'y', 'left', 'top', 'right', 'bottom', 'width', 'height'].some((k) => rect1[k] !== rect2[k])
}
/**
* Get our element bounding rectangle
*/
get boundingRect(): DOMElementBoundingRect {
return this._boundingRect
}
/**
* Set our element bounding rectangle
* @param boundingRect - new bounding rectangle
*/
set boundingRect(boundingRect: DOMElementBoundingRect) {
const isSameRect = !!this.boundingRect && this.compareBoundingRect(boundingRect, this.boundingRect)
this._boundingRect = {
top: boundingRect.top,
right: boundingRect.right,
bottom: boundingRect.bottom,
left: boundingRect.left,
width: boundingRect.width,
height: boundingRect.height,
x: boundingRect.x,
y: boundingRect.y,
}
if (!isSameRect) {
this.onSizeChanged(this.boundingRect)
}
}
/**
* Update our element bounding rectangle because the scroll position has changed
* @param delta - scroll delta values along X and Y axis
*/
updateScrollPosition(delta: DOMPosition = { x: 0, y: 0 }) {
if (this.isResizing) return
this._boundingRect.top += delta.y
this._boundingRect.left += delta.x
if (delta.x || delta.y) {
this.onPositionChanged(this.boundingRect)
}
}
/**
* Set our element bounding rectangle, either by a value or a getBoundingClientRect call
* @param boundingRect - new bounding rectangle
*/
setSize(boundingRect: DOMElementBoundingRect | null = null) {
if (!this.element) return
this.boundingRect = boundingRect ?? this.element.getBoundingClientRect()
this.isResizing = false
}
/**
* Destroy our DOMElement - remove from resize observer and clear throttle timeout
*/
destroy() {
this.resizeManager.unobserve(this.element)
}
}