@itfin/components 1.3.34 → 1.3.35

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.
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Sticky.js
3
+ * Library for sticky elements written in vanilla javascript. With this library you can easily set sticky elements on your website. It's also responsive.
4
+ *
5
+ * @version 1.3.0
6
+ * @author Rafal Galus <biuro@rafalgalus.pl>
7
+ * @website https://rgalus.github.io/sticky-js/
8
+ * @repo https://github.com/rgalus/sticky-js
9
+ * @license https://github.com/rgalus/sticky-js/blob/master/LICENSE
10
+ */
11
+ export default
12
+ class Sticky {
13
+ /**
14
+ * Sticky instance constructor
15
+ * @constructor
16
+ * @param {string} selector - Selector which we can find elements
17
+ * @param {string} options - Global options for sticky elements (could be overwritten by data-{option}="" attributes)
18
+ */
19
+ constructor(selector = '', options = {}) {
20
+ this.selector = selector;
21
+ this.elements = [];
22
+
23
+ this.version = '1.3.0';
24
+
25
+ this.vp = this.getViewportSize();
26
+ this.body = document.querySelector('body');
27
+
28
+ this.options = {
29
+ wrap: options.wrap || false,
30
+ wrapWith: options.wrapWith || '<span></span>',
31
+ marginTop: options.marginTop || 0,
32
+ marginBottom: options.marginBottom || 0,
33
+ stickyFor: options.stickyFor || 0,
34
+ stickyClass: options.stickyClass || null,
35
+ stickyContainer: options.stickyContainer || 'body',
36
+ };
37
+
38
+ this.updateScrollTopPosition = this.updateScrollTopPosition.bind(this);
39
+
40
+ this.updateScrollTopPosition();
41
+ window.addEventListener('load', this.updateScrollTopPosition);
42
+ window.addEventListener('scroll', this.updateScrollTopPosition);
43
+
44
+ this.run();
45
+ }
46
+
47
+
48
+ /**
49
+ * Function that waits for page to be fully loaded and then renders & activates every sticky element found with specified selector
50
+ * @function
51
+ */
52
+ run() {
53
+ // wait for page to be fully loaded
54
+ const pageLoaded = setInterval(() => {
55
+ if (document.readyState === 'complete') {
56
+ clearInterval(pageLoaded);
57
+
58
+ const elements = document.querySelectorAll(this.selector);
59
+ this.forEach(elements, (element) => this.renderElement(element));
60
+ }
61
+ }, 10);
62
+ }
63
+
64
+
65
+ /**
66
+ * Function that assign needed variables for sticky element, that are used in future for calculations and other
67
+ * @function
68
+ * @param {node} element - Element to be rendered
69
+ */
70
+ renderElement(element) {
71
+ // create container for variables needed in future
72
+ element.sticky = {};
73
+
74
+ // set default variables
75
+ element.sticky.active = false;
76
+
77
+ element.sticky.marginTop = parseInt(element.getAttribute('data-margin-top')) || this.options.marginTop;
78
+ element.sticky.marginBottom = parseInt(element.getAttribute('data-margin-bottom')) || this.options.marginBottom;
79
+ element.sticky.stickyFor = parseInt(element.getAttribute('data-sticky-for')) || this.options.stickyFor;
80
+ element.sticky.stickyClass = element.getAttribute('data-sticky-class') || this.options.stickyClass;
81
+ element.sticky.wrap = element.hasAttribute('data-sticky-wrap') ? true : this.options.wrap;
82
+ // @todo attribute for stickyContainer
83
+ // element.sticky.stickyContainer = element.getAttribute('data-sticky-container') || this.options.stickyContainer;
84
+ element.sticky.stickyContainer = this.options.stickyContainer;
85
+
86
+ element.sticky.container = this.getStickyContainer(element);
87
+ element.sticky.container.rect = this.getRectangle(element.sticky.container);
88
+
89
+ element.sticky.rect = this.getRectangle(element);
90
+
91
+ // fix when element is image that has not yet loaded and width, height = 0
92
+ if (element.tagName.toLowerCase() === 'img') {
93
+ element.onload = () => element.sticky.rect = this.getRectangle(element);
94
+ }
95
+
96
+ if (element.sticky.wrap) {
97
+ this.wrapElement(element);
98
+ }
99
+
100
+ // activate rendered element
101
+ this.activate(element);
102
+ }
103
+
104
+
105
+ /**
106
+ * Wraps element into placeholder element
107
+ * @function
108
+ * @param {node} element - Element to be wrapped
109
+ */
110
+ wrapElement(element) {
111
+ element.insertAdjacentHTML('beforebegin', element.getAttribute('data-sticky-wrapWith') || this.options.wrapWith);
112
+ element.previousSibling.appendChild(element);
113
+ }
114
+
115
+
116
+ /**
117
+ * Function that activates element when specified conditions are met and then initalise events
118
+ * @function
119
+ * @param {node} element - Element to be activated
120
+ */
121
+ activate(element) {
122
+ if (
123
+ ((element.sticky.rect.top + element.sticky.rect.height) < (element.sticky.container.rect.top + element.sticky.container.rect.height))
124
+ && (element.sticky.stickyFor < this.vp.width)
125
+ && !element.sticky.active
126
+ ) {
127
+ element.sticky.active = true;
128
+ }
129
+
130
+ if (this.elements.indexOf(element) < 0) {
131
+ this.elements.push(element);
132
+ }
133
+
134
+ if (!element.sticky.resizeEvent) {
135
+ this.initResizeEvents(element);
136
+ element.sticky.resizeEvent = true;
137
+ }
138
+
139
+ if (!element.sticky.scrollEvent) {
140
+ this.initScrollEvents(element);
141
+ element.sticky.scrollEvent = true;
142
+ }
143
+
144
+ this.setPosition(element);
145
+ }
146
+
147
+
148
+ /**
149
+ * Function which is adding onResizeEvents to window listener and assigns function to element as resizeListener
150
+ * @function
151
+ * @param {node} element - Element for which resize events are initialised
152
+ */
153
+ initResizeEvents(element) {
154
+ element.sticky.resizeListener = () => this.onResizeEvents(element);
155
+ window.addEventListener('resize', element.sticky.resizeListener);
156
+ }
157
+
158
+
159
+ /**
160
+ * Removes element listener from resize event
161
+ * @function
162
+ * @param {node} element - Element from which listener is deleted
163
+ */
164
+ destroyResizeEvents(element) {
165
+ window.removeEventListener('resize', element.sticky.resizeListener);
166
+ }
167
+
168
+
169
+ /**
170
+ * Function which is fired when user resize window. It checks if element should be activated or deactivated and then run setPosition function
171
+ * @function
172
+ * @param {node} element - Element for which event function is fired
173
+ */
174
+ onResizeEvents(element) {
175
+ this.vp = this.getViewportSize();
176
+
177
+ element.sticky.rect = this.getRectangle(element);
178
+ element.sticky.container.rect = this.getRectangle(element.sticky.container);
179
+
180
+ if (
181
+ ((element.sticky.rect.top + element.sticky.rect.height) < (element.sticky.container.rect.top + element.sticky.container.rect.height))
182
+ && (element.sticky.stickyFor < this.vp.width)
183
+ && !element.sticky.active
184
+ ) {
185
+ element.sticky.active = true;
186
+ } else if (
187
+ ((element.sticky.rect.top + element.sticky.rect.height) >= (element.sticky.container.rect.top + element.sticky.container.rect.height))
188
+ || element.sticky.stickyFor >= this.vp.width
189
+ && element.sticky.active
190
+ ) {
191
+ element.sticky.active = false;
192
+ }
193
+
194
+ this.setPosition(element);
195
+ }
196
+
197
+
198
+ /**
199
+ * Function which is adding onScrollEvents to window listener and assigns function to element as scrollListener
200
+ * @function
201
+ * @param {node} element - Element for which scroll events are initialised
202
+ */
203
+ initScrollEvents(element) {
204
+ element.sticky.scrollListener = () => this.onScrollEvents(element);
205
+ window.addEventListener('scroll', element.sticky.scrollListener);
206
+ }
207
+
208
+
209
+ /**
210
+ * Removes element listener from scroll event
211
+ * @function
212
+ * @param {node} element - Element from which listener is deleted
213
+ */
214
+ destroyScrollEvents(element) {
215
+ window.removeEventListener('scroll', element.sticky.scrollListener);
216
+ }
217
+
218
+
219
+ /**
220
+ * Function which is fired when user scroll window. If element is active, function is invoking setPosition function
221
+ * @function
222
+ * @param {node} element - Element for which event function is fired
223
+ */
224
+ onScrollEvents(element) {
225
+ if (element.sticky && element.sticky.active) {
226
+ this.setPosition(element);
227
+ }
228
+ }
229
+
230
+
231
+ /**
232
+ * Main function for the library. Here are some condition calculations and css appending for sticky element when user scroll window
233
+ * @function
234
+ * @param {node} element - Element that will be positioned if it's active
235
+ */
236
+ setPosition(element) {
237
+ this.css(element, { position: '', width: '', top: '', left: '' });
238
+
239
+ if ((this.vp.height < element.sticky.rect.height) || !element.sticky.active) {
240
+ return;
241
+ }
242
+
243
+ if (!element.sticky.rect.width) {
244
+ element.sticky.rect = this.getRectangle(element);
245
+ }
246
+
247
+ if (element.sticky.wrap) {
248
+ this.css(element.parentNode, {
249
+ display: 'block',
250
+ width: element.sticky.rect.width + 'px',
251
+ height: element.sticky.rect.height + 'px',
252
+ });
253
+ }
254
+
255
+ const translateY = Math.floor(this.scrollTop - element.sticky.rect.top - element.sticky.marginTop);
256
+ if (
257
+ element.sticky.rect.top === 0
258
+ && element.sticky.container === this.body
259
+ ) {
260
+ this.css(element, {
261
+ position: 'relative',
262
+ transform: `translate3d(0px, ${translateY}px, 0px)`,
263
+ // top: element.sticky.rect.top + 'px',
264
+ // left: element.sticky.rect.left + 'px',
265
+ width: element.sticky.rect.width + 'px',
266
+ top: 0
267
+ });
268
+ if (element.sticky.stickyClass) {
269
+ element.classList.add(element.sticky.stickyClass);
270
+ }
271
+ } else if (this.scrollTop > (element.sticky.rect.top - element.sticky.marginTop)) {
272
+ this.css(element, {
273
+ position: 'relative',
274
+ transform: `translate3d(0px, ${translateY}px, 0px)`,
275
+ width: element.sticky.rect.width + 'px',
276
+ top: 0
277
+ // left: element.sticky.rect.left + 'px',
278
+ });
279
+
280
+ if (
281
+ (this.scrollTop + element.sticky.rect.height + element.sticky.marginTop)
282
+ > (element.sticky.container.rect.top + element.sticky.container.offsetHeight - element.sticky.marginBottom)
283
+ ) {
284
+
285
+ if (element.sticky.stickyClass) {
286
+ // element.classList.remove(element.sticky.stickyClass);
287
+ }
288
+
289
+ this.css(element, {
290
+ top: (element.sticky.container.rect.top + element.sticky.container.offsetHeight) - (this.scrollTop + element.sticky.rect.height + element.sticky.marginBottom) + 'px' }
291
+ );
292
+ } else {
293
+ if (element.sticky.stickyClass) {
294
+ element.classList.add(element.sticky.stickyClass);
295
+ }
296
+
297
+ this.css(element, { top: element.sticky.marginTop + 'px' });
298
+ }
299
+ } else {
300
+ if (element.sticky.stickyClass) {
301
+ element.classList.remove(element.sticky.stickyClass);
302
+ }
303
+
304
+ this.css(element, { transform: '', position: '', width: '', top: '', left: '' });
305
+
306
+ if (element.sticky.wrap) {
307
+ this.css(element.parentNode, { display: '', width: '', height: '' });
308
+ }
309
+ }
310
+ }
311
+
312
+
313
+ /**
314
+ * Function that updates element sticky rectangle (with sticky container), then activate or deactivate element, then update position if it's active
315
+ * @function
316
+ */
317
+ update() {
318
+ this.forEach(this.elements, (element) => {
319
+ element.sticky.rect = this.getRectangle(element);
320
+ element.sticky.container.rect = this.getRectangle(element.sticky.container);
321
+
322
+ this.activate(element);
323
+ this.setPosition(element);
324
+ });
325
+ }
326
+
327
+
328
+ /**
329
+ * Destroys sticky element, remove listeners
330
+ * @function
331
+ */
332
+ destroy() {
333
+ window.removeEventListener('load', this.updateScrollTopPosition);
334
+ window.removeEventListener('scroll', this.updateScrollTopPosition);
335
+
336
+ this.forEach(this.elements, (element) => {
337
+ this.destroyResizeEvents(element);
338
+ this.destroyScrollEvents(element);
339
+ delete element.sticky;
340
+ });
341
+ }
342
+
343
+
344
+ /**
345
+ * Function that returns container element in which sticky element is stuck (if is not specified, then it's stuck to body)
346
+ * @function
347
+ * @param {node} element - Element which sticky container are looked for
348
+ * @return {node} element - Sticky container
349
+ */
350
+ getStickyContainer(element) {
351
+ let container = element.parentNode;
352
+
353
+ while (
354
+ !container.hasAttribute('data-sticky-container')
355
+ && !container.parentNode.querySelector(element.sticky.stickyContainer)
356
+ && container !== this.body
357
+ ) {
358
+ container = container.parentNode;
359
+ }
360
+
361
+ return container;
362
+ }
363
+
364
+
365
+ /**
366
+ * Function that returns element rectangle & position (width, height, top, left)
367
+ * @function
368
+ * @param {node} element - Element which position & rectangle are returned
369
+ * @return {object}
370
+ */
371
+ getRectangle(element) {
372
+ this.css(element, { position: '', width: '', top: '', left: '' });
373
+
374
+ const width = Math.max(element.offsetWidth, element.clientWidth, element.scrollWidth);
375
+ const height = Math.max(element.offsetHeight, element.clientHeight, element.scrollHeight);
376
+
377
+ let top = 0;
378
+ let left = 0;
379
+
380
+ do {
381
+ top += element.offsetTop || 0;
382
+ left += element.offsetLeft || 0;
383
+ element = element.offsetParent;
384
+ } while(element);
385
+
386
+ return { top, left, width, height };
387
+ }
388
+
389
+
390
+ /**
391
+ * Function that returns viewport dimensions
392
+ * @function
393
+ * @return {object}
394
+ */
395
+ getViewportSize() {
396
+ return {
397
+ width: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
398
+ height: Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
399
+ };
400
+ }
401
+
402
+
403
+ /**
404
+ * Function that updates window scroll position
405
+ * @function
406
+ * @return {number}
407
+ */
408
+ updateScrollTopPosition() {
409
+ this.scrollTop = (window.pageYOffset || document.scrollTop) - (document.clientTop || 0) || 0;
410
+ }
411
+
412
+
413
+ /**
414
+ * Helper function for loops
415
+ * @helper
416
+ * @param {array}
417
+ * @param {function} callback - Callback function (no need for explanation)
418
+ */
419
+ forEach(array, callback) {
420
+ for (let i = 0, len = array.length; i < len; i++) {
421
+ callback(array[i]);
422
+ }
423
+ }
424
+
425
+
426
+ /**
427
+ * Helper function to add/remove css properties for specified element.
428
+ * @helper
429
+ * @param {node} element - DOM element
430
+ * @param {object} properties - CSS properties that will be added/removed from specified element
431
+ */
432
+ css(element, properties) {
433
+ for (let property in properties) {
434
+ if (properties.hasOwnProperty(property)) {
435
+ element.style[property] = properties[property];
436
+ }
437
+ }
438
+ }
439
+ }