@smartimpact-it/scroll-utils 1.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.
Files changed (49) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +158 -0
  3. package/dist/cjs/Bootstrap4AccordionScrollIntoView.js +74 -0
  4. package/dist/cjs/BootstrapAccordionScrollIntoView.js +68 -0
  5. package/dist/cjs/FixHashScrollPosition.js +38 -0
  6. package/dist/cjs/FoundationAccordionScrollIntoView.js +52 -0
  7. package/dist/cjs/ScrollDirection.js +120 -0
  8. package/dist/cjs/ScrollOffset.js +346 -0
  9. package/dist/cjs/ScrollPages.js +97 -0
  10. package/dist/cjs/index.js +93 -0
  11. package/dist/cjs/scrollWithMarginTop.js +34 -0
  12. package/dist/cjs/utils/onReady.js +46 -0
  13. package/dist/cjs/utils/utils.js +77 -0
  14. package/dist/esm/Bootstrap4AccordionScrollIntoView.js +57 -0
  15. package/dist/esm/BootstrapAccordionScrollIntoView.js +51 -0
  16. package/dist/esm/FixHashScrollPosition.js +24 -0
  17. package/dist/esm/FoundationAccordionScrollIntoView.js +35 -0
  18. package/dist/esm/ScrollDirection.js +98 -0
  19. package/dist/esm/ScrollOffset.js +283 -0
  20. package/dist/esm/ScrollPages.js +80 -0
  21. package/dist/esm/index.js +9 -0
  22. package/dist/esm/scrollWithMarginTop.js +25 -0
  23. package/dist/esm/utils/onReady.js +33 -0
  24. package/dist/esm/utils/utils.js +62 -0
  25. package/dist/types/Bootstrap4AccordionScrollIntoView.d.ts +11 -0
  26. package/dist/types/BootstrapAccordionScrollIntoView.d.ts +11 -0
  27. package/dist/types/FixHashScrollPosition.d.ts +7 -0
  28. package/dist/types/FoundationAccordionScrollIntoView.d.ts +11 -0
  29. package/dist/types/ScrollDirection.d.ts +29 -0
  30. package/dist/types/ScrollOffset.d.ts +49 -0
  31. package/dist/types/ScrollPages.d.ts +21 -0
  32. package/dist/types/index.d.ts +9 -0
  33. package/dist/types/scrollWithMarginTop.d.ts +5 -0
  34. package/dist/types/utils/onReady.d.ts +20 -0
  35. package/dist/types/utils/utils.d.ts +4 -0
  36. package/dist/umd/index.js +2 -0
  37. package/dist/umd/index.js.LICENSE.txt +8 -0
  38. package/package.json +101 -0
  39. package/src/Bootstrap4AccordionScrollIntoView.ts +51 -0
  40. package/src/BootstrapAccordionScrollIntoView.ts +45 -0
  41. package/src/FixHashScrollPosition.ts +31 -0
  42. package/src/FoundationAccordionScrollIntoView.ts +31 -0
  43. package/src/ScrollDirection.ts +122 -0
  44. package/src/ScrollOffset.ts +316 -0
  45. package/src/ScrollPages.ts +81 -0
  46. package/src/index.js +9 -0
  47. package/src/scrollWithMarginTop.ts +33 -0
  48. package/src/utils/onReady.ts +38 -0
  49. package/src/utils/utils.ts +78 -0
@@ -0,0 +1,45 @@
1
+ import { scrollWithMarginTop } from './scrollWithMarginTop';
2
+ import { onComplete } from './utils/onReady';
3
+
4
+ /**
5
+ * Scroll to the opened accordion tab
6
+ */
7
+ export class BootstrapAccordionScrollIntoView {
8
+ extraOffset: number;
9
+
10
+ constructor({ extraOffset = 0 } = {}) {
11
+ this.extraOffset = extraOffset;
12
+ onComplete(() => setTimeout(() => this.#addEventListeners(), 1000));
13
+ }
14
+
15
+ #addEventListeners() {
16
+ document.addEventListener('shown.bs.collapse', (e: Event) => {
17
+ this.#handle(e);
18
+ });
19
+ }
20
+
21
+ #handle = (e: Event) => {
22
+ let header: HTMLElement = null;
23
+ const target = e.target as unknown as HTMLElement;
24
+
25
+ // Skip scrolling if the clicked element or any of its parents has the class 'skip-scroll-into-view'.
26
+ if (target.closest?.('.skip-scroll-into-view, [data-skip-scroll-into-view]')) {
27
+ return;
28
+ }
29
+
30
+ const tabId = target.id;
31
+ const headerId = target.getAttribute('aria-labelledby');
32
+ if (headerId) {
33
+ header = document.querySelector(`#${headerId}`);
34
+ }
35
+ if (!header && tabId) {
36
+ header = document.querySelector(`[data-bs-target="#${tabId}"]`);
37
+ }
38
+
39
+ if (header) {
40
+ scrollWithMarginTop(header, this.extraOffset, true);
41
+ }
42
+ };
43
+ }
44
+
45
+ export default BootstrapAccordionScrollIntoView;
@@ -0,0 +1,31 @@
1
+ import { scrollWithMarginTop } from './scrollWithMarginTop';
2
+
3
+ /**
4
+ * Fix the scroll offset when loading a page with a #hash (anchor)
5
+ */
6
+ export class FixHashScrollPosition {
7
+ constructor() {
8
+ // fix for scroll-margin-top
9
+ window.addEventListener(
10
+ 'hashchange',
11
+ () => {
12
+ const hash = window.location.hash;
13
+ if (!hash || hash === '#') {
14
+ return;
15
+ }
16
+
17
+ try {
18
+ const element = document.querySelector(hash) as HTMLElement;
19
+ if (element) {
20
+ setTimeout(() => scrollWithMarginTop(element));
21
+ }
22
+ } catch (e) {
23
+ console.warn(e);
24
+ }
25
+ },
26
+ false
27
+ );
28
+ }
29
+ }
30
+
31
+ export default FixHashScrollPosition;
@@ -0,0 +1,31 @@
1
+ import { scrollWithMarginTop } from './scrollWithMarginTop';
2
+ import { onComplete } from './utils/onReady';
3
+
4
+ /**
5
+ * Scroll to the opened accordion tab
6
+ */
7
+ export class FoundationAccordionScrollIntoView {
8
+ extraOffset: number;
9
+
10
+ constructor({ extraOffset = 0 } = {}) {
11
+ this.extraOffset = extraOffset;
12
+ onComplete(() => setTimeout(() => this.#addEventListeners(), 1000));
13
+ }
14
+
15
+ #addEventListeners() {
16
+ if (!('$' in window)) return;
17
+
18
+ (window as any).$(document).on('down.zf.accordion', (e, $content) => {
19
+ const target = $content.get(0).parentNode;
20
+
21
+ // Skip scrolling if the clicked element or any of its parents has the class 'skip-scroll-into-view'.
22
+ if (target.closest?.('.skip-scroll-into-view, [data-skip-scroll-into-view]')) {
23
+ return;
24
+ }
25
+
26
+ scrollWithMarginTop(target, this.extraOffset, true);
27
+ });
28
+ }
29
+ }
30
+
31
+ export default FoundationAccordionScrollIntoView;
@@ -0,0 +1,122 @@
1
+ import isFunction from 'lodash/isFunction';
2
+ import throttle from 'lodash/throttle';
3
+
4
+ /**
5
+ * ScrollDirection - a class to handle scroll direction classes on body
6
+ *
7
+ * Author: Bogdan Barbu
8
+ * Team: Codingheads (codingheads.com)
9
+ */
10
+
11
+ interface ThrottleSettings {
12
+ leading?: boolean | undefined;
13
+ trailing?: boolean | undefined;
14
+ }
15
+
16
+ interface ScrollDirectionOptions {
17
+ onlyFor?: () => boolean | null;
18
+ threshold?: number;
19
+ thresholdCallback?: Function | null;
20
+ throttle?: null | number;
21
+ throttleOptions?: ThrottleSettings;
22
+ }
23
+
24
+ export class ScrollDirection {
25
+ onlyForCallback: () => boolean | null = null;
26
+ #lastScrollTop: number = 0;
27
+ threshold: number = 0;
28
+ thresholdCallback?: Function | null = null;
29
+ #throttle?: null | number;
30
+ #throttleOptions: ThrottleSettings;
31
+
32
+ /**
33
+ * start an instance
34
+ * @param {{onlyFor: a callback to determine where to modify classes; threshold: amount in px to take into account }} param0
35
+ */
36
+ constructor({
37
+ onlyFor = null,
38
+ threshold = 0,
39
+ thresholdCallback = null,
40
+ throttle = 16,
41
+ throttleOptions = {
42
+ trailing: false,
43
+ },
44
+ }: ScrollDirectionOptions = {}) {
45
+ this.onlyForCallback = onlyFor;
46
+ this.threshold = threshold;
47
+ this.thresholdCallback = thresholdCallback;
48
+ this.#throttle = throttle;
49
+ this.#throttleOptions = throttleOptions;
50
+ this.#lastScrollTop = window.pageYOffset || document.documentElement.scrollTop;
51
+
52
+ this.#setupListeners();
53
+ }
54
+
55
+ /**
56
+ * setup the scroll event listener
57
+ */
58
+ #setupListeners() {
59
+ const update = () => {
60
+ const isValid = !this.onlyForCallback || this.onlyForCallback();
61
+ if (!isValid) return;
62
+ this.#determineDirection();
63
+ };
64
+ if (this.#throttle) {
65
+ const throttledUpdate = throttle(
66
+ update,
67
+ this.#throttle,
68
+ this.#throttleOptions || {}
69
+ );
70
+ window.addEventListener('scroll', throttledUpdate, { passive: true });
71
+ } else {
72
+ window.addEventListener('scroll', update, { passive: true });
73
+ }
74
+ }
75
+
76
+ /**
77
+ * dispatch an event on the window
78
+ */
79
+ #dispatchEvent(name: string) {
80
+ requestAnimationFrame(() => {
81
+ window.dispatchEvent(new CustomEvent(name));
82
+ });
83
+ }
84
+
85
+ /**
86
+ * determine the scroll direction
87
+ */
88
+ #determineDirection() {
89
+ const body = document.body,
90
+ classList = body.classList;
91
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
92
+ const localThreshold =
93
+ this.thresholdCallback && isFunction(this.thresholdCallback)
94
+ ? this.thresholdCallback({
95
+ scrollTop,
96
+ threshold: this.threshold,
97
+ lastScrollTop: this.#lastScrollTop,
98
+ })
99
+ : this.threshold;
100
+ if (Math.abs(scrollTop - this.#lastScrollTop) < localThreshold) return;
101
+
102
+ if (
103
+ scrollTop > this.#lastScrollTop + localThreshold &&
104
+ !classList.contains('scrolling-down')
105
+ ) {
106
+ classList.add('scrolling-down');
107
+ classList.remove('scrolling-up');
108
+ this.#dispatchEvent('scrollDirectionChange');
109
+ } else if (
110
+ scrollTop < this.#lastScrollTop - localThreshold &&
111
+ !classList.contains('scrolling-up')
112
+ ) {
113
+ classList.add('scrolling-up');
114
+ classList.remove('scrolling-down');
115
+ this.#dispatchEvent('scrollDirectionChange');
116
+ }
117
+
118
+ this.#lastScrollTop = Math.max(scrollTop, 0);
119
+ }
120
+ }
121
+
122
+ export default ScrollDirection;
@@ -0,0 +1,316 @@
1
+ import debounce from 'lodash/debounce';
2
+ import isFunction from 'lodash/isFunction';
3
+
4
+ export type ScrollOffsetPartSettings = {
5
+ name?: string;
6
+ selectors?: string[];
7
+ elements?: HTMLElement[];
8
+ fixedHeight?: number | false;
9
+ condition?: Function | false;
10
+ resizeCondition?: Function | false;
11
+ };
12
+
13
+ export type ScrollOffsetVariable = {
14
+ name: string;
15
+ offsetParts: Array<string | ScrollOffsetPart>;
16
+ };
17
+
18
+ export type ScrollOffsetMap = {
19
+ [key: string]: Array<string | ScrollOffsetPart>;
20
+ };
21
+
22
+ export type ScrollOffsetSettings = {
23
+ offsetParts?: ScrollOffsetPart[] | ScrollOffsetPartSettings[];
24
+ variables?: ScrollOffsetVariable[] | ScrollOffsetMap;
25
+ extraEvents?: string[];
26
+ registerForHoudini?: boolean;
27
+ useResizeObserver?: boolean;
28
+ };
29
+
30
+ export class ScrollOffsetPart {
31
+ #name: string;
32
+ #selectors: string[];
33
+ #elements: HTMLElement[];
34
+ #fixedHeight: number | false;
35
+ #condition: Function | false;
36
+ #resizeCondition: Function | false;
37
+
38
+ #resizeConditionValue: boolean;
39
+ #totalHeight: number = 0;
40
+ #isValid: boolean = false;
41
+ #heightCache = new WeakMap<HTMLElement, number>();
42
+
43
+ constructor({
44
+ name = '',
45
+ selectors = [],
46
+ elements = [],
47
+ fixedHeight = false,
48
+ condition = false,
49
+ resizeCondition = false,
50
+ }: ScrollOffsetPartSettings) {
51
+ this.#name = name;
52
+ this.#selectors = (selectors || []).filter(Boolean);
53
+ this.#elements = (elements || []).filter(Boolean);
54
+ this.#fixedHeight = fixedHeight;
55
+ this.#condition = condition;
56
+ this.#resizeCondition = resizeCondition;
57
+
58
+ this.#selectors.forEach(selector => {
59
+ const newElements = [...document.querySelectorAll(selector)] as HTMLElement[];
60
+ if (newElements.length) {
61
+ this.#elements = [...this.#elements, ...newElements];
62
+ }
63
+ });
64
+
65
+ this.calculate();
66
+ }
67
+
68
+ get name(): string {
69
+ return this.#name;
70
+ }
71
+
72
+ get totalHeight(): number {
73
+ return this.#totalHeight;
74
+ }
75
+
76
+ get isValid(): boolean {
77
+ return this.#isValid;
78
+ }
79
+
80
+ get elements(): HTMLElement[] {
81
+ return this.#elements;
82
+ }
83
+
84
+ setElementHeight = (element: HTMLElement, height: number) => {
85
+ if (!element || !Number.isFinite(height) || height < 0) return;
86
+ this.#heightCache.set(element, Math.round(height));
87
+ };
88
+
89
+ #getElementHeight = (element: HTMLElement) => {
90
+ if (!element) return 0;
91
+ const cachedHeight = this.#heightCache.get(element);
92
+ if (cachedHeight !== undefined) return cachedHeight;
93
+
94
+ const measuredHeight = element.clientHeight || 0;
95
+ this.#heightCache.set(element, measuredHeight);
96
+ return measuredHeight;
97
+ };
98
+
99
+ calculate = (): this => {
100
+ this.#isValid = !!this.#elements?.length;
101
+ if (this.#isValid && this.#condition && isFunction(this.#condition)) {
102
+ this.#isValid = this.#isValid && this.#condition();
103
+ }
104
+ if (this.#isValid && this.#resizeCondition && isFunction(this.#resizeCondition)) {
105
+ if (!('resizeConditionValue' in this)) {
106
+ this.#resizeConditionValue = this.#resizeCondition();
107
+ }
108
+ this.#isValid = this.#isValid && this.#resizeConditionValue;
109
+ }
110
+
111
+ if (!this.#isValid) {
112
+ this.#totalHeight = 0;
113
+ } else if (this.#fixedHeight) {
114
+ this.#totalHeight = this.#fixedHeight;
115
+ } else {
116
+ let totalHeight = 0;
117
+ this.#elements.forEach(element => {
118
+ totalHeight += this.#getElementHeight(element);
119
+ });
120
+ this.#totalHeight = totalHeight;
121
+ }
122
+
123
+ return this;
124
+ };
125
+
126
+ calculateResizeConditions = (): boolean => {
127
+ if (this.#resizeCondition && isFunction(this.#resizeCondition)) {
128
+ const oldValue = this.#resizeConditionValue || false;
129
+ this.#resizeConditionValue = this.#resizeCondition();
130
+ return oldValue != this.#resizeConditionValue;
131
+ }
132
+ return false;
133
+ };
134
+ }
135
+
136
+ /**!
137
+ * ScrollOffset - a class to handle add css variables to the body element,
138
+ * with the value of the "header" parts that are visible
139
+ * so that we can adjust scroll positions etc. based on it
140
+ *
141
+ * Author: Bogdan Barbu
142
+ * Team: Codingheads (codingheads.com)
143
+ */
144
+ export class ScrollOffset {
145
+ #offsetParts: ScrollOffsetPart[];
146
+ #variables: ScrollOffsetVariable[];
147
+ #extraEvents: string[];
148
+ #registerForHoudini: boolean;
149
+ #useResizeObserver: boolean;
150
+ #measureScheduled: boolean = false;
151
+
152
+ /**
153
+ * setup the conditions
154
+ */
155
+ constructor({
156
+ offsetParts = [],
157
+ variables = [],
158
+ registerForHoudini = true,
159
+ useResizeObserver = true,
160
+ extraEvents = [],
161
+ }: ScrollOffsetSettings = {}) {
162
+ // parse the variables
163
+ if (Array.isArray(variables)) {
164
+ this.#variables = variables;
165
+ } else {
166
+ this.#variables = Object.entries(variables).map(([key, value]) => {
167
+ return {
168
+ name: key,
169
+ offsetParts: value,
170
+ };
171
+ });
172
+ }
173
+
174
+ // take the offset parts from the variables
175
+ const variableOffsetParts = this.#variables
176
+ .map(({ offsetParts }) => {
177
+ return offsetParts.filter(
178
+ part => part instanceof ScrollOffsetPart
179
+ ) as ScrollOffsetPart[];
180
+ })
181
+ .flat(1);
182
+
183
+ // merge with the settings and keep only distinct elements
184
+ offsetParts = [...new Set([...offsetParts, ...variableOffsetParts])];
185
+
186
+ // parse the offset parts
187
+ this.#offsetParts = offsetParts.map(part => {
188
+ let partObject = null;
189
+ if (!(part instanceof ScrollOffsetPart)) {
190
+ partObject = new ScrollOffsetPart(part);
191
+ } else {
192
+ partObject = part;
193
+ }
194
+ partObject.calculate();
195
+ return partObject;
196
+ });
197
+
198
+ this.#extraEvents = extraEvents || [];
199
+ this.#registerForHoudini =
200
+ registerForHoudini && 'CSS' in window && 'registerProperty' in window.CSS;
201
+ this.#useResizeObserver = 'ResizeObserver' in window && useResizeObserver;
202
+
203
+ if (this.#registerForHoudini) {
204
+ this.#registerCssVariables();
205
+ }
206
+
207
+ this.#setupListeners();
208
+ this.#calculateOffset();
209
+ }
210
+
211
+ /**
212
+ * register the css variables as length in the Houdini CSS API
213
+ */
214
+ #registerCssVariables = () => {
215
+ this.#variables.forEach(({ name }) => {
216
+ (window.CSS as any).registerProperty({
217
+ name: `--${name}`,
218
+ syntax: '<length>',
219
+ inherits: true,
220
+ initialValue: '0px',
221
+ });
222
+ });
223
+ };
224
+
225
+ /**
226
+ * calculate the offsets and set the css variables
227
+ */
228
+ #calculateOffset = () => {
229
+ // calculate the conditions
230
+ this.#offsetParts.forEach(part => part.calculate());
231
+
232
+ // determine the css variables
233
+ const cssVariables = this.#variables.map(variable => {
234
+ const value = variable.offsetParts.reduce((acc: number, part) => {
235
+ if (typeof part == 'string') {
236
+ part = this.#offsetParts.find(
237
+ partInfo => partInfo.name.localeCompare(part as string) == 0
238
+ );
239
+ }
240
+ if (part && part.isValid) {
241
+ acc += part.totalHeight;
242
+ }
243
+ return acc;
244
+ }, 0);
245
+ return { ...variable, value };
246
+ });
247
+
248
+ // output the variables
249
+ cssVariables.forEach(({ name, value }) => {
250
+ document.documentElement.style.setProperty(`--${name}`, `${value}px`);
251
+ });
252
+ document.body.dispatchEvent(new CustomEvent('ScrollOffsetChange', { bubbles: true }));
253
+ };
254
+
255
+ /**
256
+ * recalculate values for conditions that only depend on resize
257
+ */
258
+ #calculateResizeConditions = () => {
259
+ this.#offsetParts.forEach(part => part.calculateResizeConditions());
260
+ };
261
+
262
+ #debouncedCalculateOffset = debounce(this.#calculateOffset, 32);
263
+ #debouncedCalculateResizeConditions = debounce(this.#calculateResizeConditions, 32);
264
+
265
+ #scheduleCalculateOffset = () => {
266
+ if (this.#measureScheduled) return;
267
+ this.#measureScheduled = true;
268
+
269
+ requestAnimationFrame(() => {
270
+ this.#measureScheduled = false;
271
+ this.#debouncedCalculateOffset();
272
+ });
273
+ };
274
+
275
+ /**
276
+ * setup event listeners
277
+ */
278
+ #setupListeners = () => {
279
+ ['resize', 'orientationchange'].forEach(type => {
280
+ window.addEventListener(type, this.#debouncedCalculateResizeConditions);
281
+ });
282
+
283
+ [
284
+ 'resize',
285
+ 'orientationchange',
286
+ 'scrollDirectionChange',
287
+ 'stickyIsPinned',
288
+ 'stickyIsUnpinned',
289
+ ...this.#extraEvents,
290
+ ].forEach(type => window.addEventListener(type, this.#scheduleCalculateOffset));
291
+
292
+ // setup resizeObservers for the elements
293
+ if (this.#useResizeObserver) {
294
+ const resizeObserver = new ResizeObserver(entries => {
295
+ entries.forEach(entry => {
296
+ const target = entry.target as HTMLElement;
297
+ const height = Math.round(entry.contentRect?.height || target?.clientHeight || 0);
298
+ if (!target || !height) return;
299
+
300
+ this.#offsetParts.forEach(part => {
301
+ if (part.elements.includes(target)) {
302
+ part.setElementHeight(target, height);
303
+ }
304
+ });
305
+ });
306
+
307
+ this.#scheduleCalculateOffset();
308
+ });
309
+ this.#offsetParts.forEach(part =>
310
+ part.elements.forEach(element => resizeObserver.observe(element))
311
+ );
312
+ }
313
+ };
314
+ }
315
+
316
+ export default ScrollOffset;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * ScrollPages - a class to add classes to the body,
3
+ * detecting whether we are above the fold or below
4
+ * (the above-the-fold are can also be set as an existing element)
5
+ *
6
+ * Author: Bogdan Barbu
7
+ * Team: Codingheads (codingheads.com)
8
+ */
9
+
10
+ interface ScrollPagesOptions {
11
+ intersectionElement?: HTMLElement;
12
+ belowTheFoldClass?: string;
13
+ aboveTheFoldClass?: string;
14
+ }
15
+
16
+ export class ScrollPages {
17
+ #intersectionElement: HTMLElement;
18
+ #observer: IntersectionObserver = null;
19
+ #isBelowTheFold: boolean = false;
20
+ #belowTheFoldClass: string;
21
+ #aboveTheFoldClass: string;
22
+
23
+ /**
24
+ * start an instance
25
+ */
26
+ constructor({
27
+ intersectionElement = null,
28
+ belowTheFoldClass = 'below-the-fold',
29
+ aboveTheFoldClass = 'above-the-fold',
30
+ }: ScrollPagesOptions = {}) {
31
+ this.#intersectionElement = intersectionElement;
32
+ this.#belowTheFoldClass = belowTheFoldClass;
33
+ this.#aboveTheFoldClass = aboveTheFoldClass;
34
+ this.#setupObserver();
35
+ }
36
+
37
+ /**
38
+ * dispatch an event on the window
39
+ */
40
+ #dispatchEvent(name: string) {
41
+ requestAnimationFrame(() => {
42
+ window.dispatchEvent(new CustomEvent(name));
43
+ });
44
+ }
45
+
46
+ #setupObserver() {
47
+ const body = document.body;
48
+
49
+ // create the intersection element, if it doesn't exist
50
+ if (!this.#intersectionElement) {
51
+ let containerPosition = window.getComputedStyle(body).getPropertyValue('position');
52
+ if (!containerPosition || containerPosition == 'static') {
53
+ body.style.position = 'relative';
54
+ }
55
+ this.#intersectionElement = document.createElement('div');
56
+ this.#intersectionElement.classList.add('second-page-observer');
57
+ this.#intersectionElement.style.cssText =
58
+ 'pointerEvents: none; visibility: hidden; position: absolute; top: 0; left: 0; right: 0; height: 100vh;';
59
+ body.appendChild(this.#intersectionElement);
60
+ }
61
+ body.classList.add(this.#aboveTheFoldClass);
62
+
63
+ this.#observer = new IntersectionObserver(entries => {
64
+ let isFirstPage = true;
65
+ entries.forEach(entry => {
66
+ if (!entry.isIntersecting) {
67
+ isFirstPage = false;
68
+ }
69
+ });
70
+ if (this.#isBelowTheFold != !isFirstPage) {
71
+ this.#isBelowTheFold = !isFirstPage;
72
+ body.classList[this.#isBelowTheFold ? 'add' : 'remove'](this.#belowTheFoldClass);
73
+ body.classList[!this.#isBelowTheFold ? 'add' : 'remove'](this.#aboveTheFoldClass);
74
+ this.#dispatchEvent('ScrollPageChange');
75
+ }
76
+ });
77
+ this.#observer.observe(this.#intersectionElement);
78
+ }
79
+ }
80
+
81
+ export default ScrollPages;
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { ScrollDirection } from './ScrollDirection';
2
+ export { ScrollOffset, ScrollOffsetPart } from './ScrollOffset';
3
+ export { ScrollPages } from './ScrollPages';
4
+ export { scrollWithMarginTop } from './scrollWithMarginTop';
5
+ export { FixHashScrollPosition } from './FixHashScrollPosition';
6
+ export { BootstrapAccordionScrollIntoView } from './BootstrapAccordionScrollIntoView';
7
+ export { Bootstrap4AccordionScrollIntoView } from './Bootstrap4AccordionScrollIntoView';
8
+ export { FoundationAccordionScrollIntoView } from './FoundationAccordionScrollIntoView';
9
+ export { onReadyState, onComplete, onInteractive, onReady } from './utils/onReady';
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Scroll to an element taking into account its "scroll-margin-top"/"scroll-snap-margin-top".
3
+ * This will help with Safari < 14.1 / Safari iOS <= 1.4, which doesn't support "scroll-margin-top".
4
+ */
5
+ export const scrollWithMarginTop = (
6
+ element: HTMLElement,
7
+ offset: number = 0,
8
+ onlyWhenNeeded = false
9
+ ) => {
10
+ if (!element) return;
11
+
12
+ const rect = element.getBoundingClientRect();
13
+ const pageYOffset = window.pageYOffset;
14
+ const top = rect.top + pageYOffset;
15
+ const style = window.getComputedStyle(element);
16
+ const scrollMarginTop =
17
+ parseInt(style.getPropertyValue('scroll-margin-top')) ||
18
+ parseInt(style.getPropertyValue('scroll-snap-margin-top')) ||
19
+ 0;
20
+ const newPosition = top - scrollMarginTop + offset;
21
+
22
+ // don't scroll if the element is already visible in the first half of the screen
23
+ if (onlyWhenNeeded) {
24
+ const isVisible =
25
+ newPosition >= pageYOffset &&
26
+ newPosition + rect.height < pageYOffset + window.innerHeight / 2;
27
+ if (isVisible) {
28
+ return;
29
+ }
30
+ }
31
+
32
+ window.scrollTo({ top: newPosition, left: 0, behavior: 'smooth' });
33
+ };