Skip to content

Commit e66b987

Browse files
committed
fix(pat-bumper): Correctly set the bumpuing classes.
The bumping classes are now set correctly on any bumping direction. The logic is adapted to work with any top, right, bottom, left, margin, border and padding setting on a wrapping container, two different containers for x and y scrolling and on the window viewport as container. Fixes: #1083
1 parent a6ffe12 commit e66b987

File tree

4 files changed

+379
-287
lines changed

4 files changed

+379
-287
lines changed

src/pat/bumper/_bumper.scss

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/pat/bumper/bumper.js

Lines changed: 208 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { BasePattern } from "@patternslib/patternslib/src/core/basepattern";
22
import dom from "../../core/dom";
3+
import events from "../../core/events";
34
import Parser from "../../core/parser";
4-
import registry from "@patternslib/patternslib/src/core/registry";
5+
import registry from "../../core/registry";
56
import utils from "../../core/utils";
67

78
export const parser = new Parser("bumper");
@@ -15,121 +16,236 @@ class Pattern extends BasePattern {
1516
static name = "bumper";
1617
static trigger = ".pat-bumper";
1718
static parser = parser;
19+
ticking = false;
1820

1921
async init() {
20-
// Based on: https://davidwalsh.name/detect-sticky
21-
2222
this.target_element = this.options.selector
2323
? document.querySelector(this.options.selector)
2424
: this.el;
2525

2626
// wait for next repaint for things to settle.
2727
// e.g. CSS applied for injected content.
2828
await utils.timeout(1);
29-
this._init();
30-
}
3129

32-
_init() {
33-
const scroll_container_y = dom.find_scroll_container(
34-
this.el.parentElement,
35-
"y",
36-
null
37-
);
38-
const scroll_container_x = dom.find_scroll_container(
39-
this.el.parentElement,
40-
"x",
41-
null
42-
);
43-
44-
const pos = {
30+
const parent_el = this.el.parentElement;
31+
this.container_x = dom.find_scroll_container(parent_el, "x", null);
32+
this.container_y = dom.find_scroll_container(parent_el, "y", null);
33+
34+
// Viewport dimensions
35+
this.dim_viewport = {
36+
top:
37+
0 +
38+
dom.get_css_value(document.body, "margin-top", true) +
39+
dom.get_css_value(document.body, "padding-top", true),
40+
left:
41+
0 +
42+
dom.get_css_value(document.body, "margin-left", true) +
43+
dom.get_css_value(document.body, "padding-left", true),
44+
};
45+
this.dim_viewport.right =
46+
document.documentElement.clientWidth -
47+
dom.get_css_value(document.body, "margin-right", true) -
48+
dom.get_css_value(document.body, "padding-right", true);
49+
this.dim_viewport.bottom =
50+
document.documentElement.clientHeight -
51+
dom.get_css_value(document.body, "margin-bottom", true) -
52+
dom.get_css_value(document.body, "padding-bottom", true);
53+
54+
this.dim_element = {
4555
top: dom.get_css_value(this.el, "top", true),
4656
right: dom.get_css_value(this.el, "right", true),
4757
bottom: dom.get_css_value(this.el, "bottom", true),
4858
left: dom.get_css_value(this.el, "left", true),
59+
margin_top: dom.get_css_value(this.el, "margin-top", true),
60+
margin_bottom: dom.get_css_value(this.el, "margin-bottom", true),
61+
margin_right: dom.get_css_value(this.el, "margin-right", true),
62+
margin_left: dom.get_css_value(this.el, "margin-left", true),
4963
};
50-
const intersection_observer_config = {
51-
threshold: [1, 0.99, 0.97, 0.96, 0.95, 0.94, 0.93, 0.92, 0.91, 0.9],
52-
// add margin as inverted sticky positions.
53-
rootMargin: `${-pos.top - 1}px ${-pos.right - 1}px ${-pos.bottom - 1}px ${-pos.left - 1}px`, // prettier-ignore
54-
};
5564

56-
const observer_y = new IntersectionObserver(
57-
this._intersection_observer_callback.bind(this),
58-
{
59-
...intersection_observer_config,
60-
root: scroll_container_y,
61-
}
62-
);
63-
observer_y.observe(this.el);
64-
65-
if (scroll_container_x !== scroll_container_y) {
66-
const observer_x = new IntersectionObserver(
67-
this._intersection_observer_callback.bind(this),
68-
{
69-
...intersection_observer_config,
70-
root: scroll_container_x,
65+
this.dim_container_x = this.container_x
66+
? {
67+
border_top_width: dom.get_css_value(this.container_x, "border-top-width", true), // prettier-ignore
68+
border_left_width: dom.get_css_value(this.container_x, "border-left-width", true), // prettier-ignore
69+
padding_top: dom.get_css_value(this.container_x, "padding-top", true), // prettier-ignore
70+
padding_right: dom.get_css_value(this.container_x, "padding-right", true), // prettier-ignore
71+
padding_bottom: dom.get_css_value(this.container_x, "padding-bottom", true), // prettier-ignore
72+
padding_left: dom.get_css_value(this.container_x, "padding-left", true), // prettier-ignore
73+
}
74+
: {};
75+
76+
this.dim_container_y = this.container_y
77+
? {
78+
border_top_width: dom.get_css_value(this.container_y, "border-top-width", true), // prettier-ignore
79+
border_left_width: dom.get_css_value(this.container_y, "border-left-width", true), // prettier-ignore
80+
padding_top: dom.get_css_value(this.container_y, "padding-top", true), // prettier-ignore
81+
padding_right: dom.get_css_value(this.container_y, "padding-right", true), // prettier-ignore
82+
padding_bottom: dom.get_css_value(this.container_y, "padding-bottom", true), // prettier-ignore
83+
padding_left: dom.get_css_value(this.container_y, "padding-left", true), // prettier-ignore
84+
}
85+
: {};
86+
87+
const containers = new Set([this.container_x, this.container_y]);
88+
for (const container of containers) {
89+
events.add_event_listener(
90+
container || document,
91+
"scroll",
92+
"pat_bumper__scroll",
93+
async () => {
94+
if (!this.ticking) {
95+
this.ticking = true;
96+
await utils.animation_frame();
97+
this.set_bumping_classes();
98+
this.ticking = false;
99+
}
71100
}
72101
);
73-
observer_x.observe(this.el);
74102
}
103+
this.set_bumping_classes();
75104
}
76105

77-
_intersection_observer_callback(entries) {
78-
const el = this.target_element;
79-
for (const entry of entries) {
80-
if (entry.intersectionRatio < 1) {
81-
if (this.options.bump.add) {
82-
el.classList.add(this.options.bump.add);
83-
}
84-
if (this.options.bump.remove) {
85-
el.classList.remove(this.options.bump.remove);
86-
}
106+
/**
107+
* Get the container position values.
108+
*
109+
* @param {DOMElement} container - The container element.
110+
* @param {Objcet} dimensions - The dimension Object of the container,
111+
* which were initialized in the init method.
112+
*
113+
* @returns {Object} The position values.
114+
*/
115+
_get_container_positions(container, dimensions) {
116+
if (!container) {
117+
// No container = document.body
118+
return this.dim_viewport;
119+
}
87120

88-
const root = entry.rootBounds;
89-
if (!root) {
90-
// No root found - e.g. CSS not fully applied when scroll
91-
// container was searched - as can happen as a corner case
92-
// after injecting content and initializing this pattern in
93-
// the same repaint cycle.
94-
// This is actually prevented by the 1ms timeout in the
95-
// init method.
96-
return;
97-
}
98-
const bounds = entry.boundingClientRect;
121+
// Bounds are dynamic, so we cannot cache them.
122+
const bounds = container.getBoundingClientRect();
99123

100-
if (bounds.left <= root.left) {
101-
el.classList.add("bumped-left");
102-
} else {
103-
el.classList.remove("bumped-left");
104-
}
105-
if (bounds.top <= root.top) {
106-
el.classList.add("bumped-top");
107-
} else {
108-
el.classList.remove("bumped-top");
109-
}
110-
if (bounds.right >= root.right) {
111-
el.classList.add("bumped-right");
112-
} else {
113-
el.classList.remove("bumped-right");
114-
}
115-
if (bounds.bottom >= root.bottom) {
116-
el.classList.add("bumped-bottom");
117-
} else {
118-
el.classList.remove("bumped-bottom");
119-
}
120-
} else {
121-
if (this.options.unbump.add) {
122-
el.classList.add(this.options.unbump.add);
123-
}
124-
if (this.options.unbump.remove) {
125-
el.classList.remove(this.options.unbump.remove);
126-
}
127-
el.classList.remove("bumped-left");
128-
el.classList.remove("bumped-top");
129-
el.classList.remove("bumped-right");
130-
el.classList.remove("bumped-bottom");
131-
}
124+
const left =
125+
bounds.left +
126+
dimensions.border_left_width +
127+
dimensions.padding_left; // prettier-ignore
128+
const top =
129+
bounds.top +
130+
dimensions.border_top_width +
131+
dimensions.padding_top; // prettier-ignore
132+
133+
const right =
134+
bounds.left +
135+
dimensions.border_left_width +
136+
container.clientWidth -
137+
dimensions.padding_right;
138+
139+
const bottom =
140+
bounds.top +
141+
dimensions.border_top_width +
142+
container.clientHeight -
143+
dimensions.padding_bottom;
144+
145+
return {
146+
top: Math.round(top),
147+
right: Math.round(right),
148+
bottom: Math.round(bottom),
149+
left: Math.round(left),
150+
};
151+
}
152+
153+
/**
154+
* Get the element position values.
155+
*
156+
* @returns {Object} The position values.
157+
*/
158+
_get_element_positions() {
159+
const bounds = this.el.getBoundingClientRect();
160+
return {
161+
top: Math.round(
162+
bounds.top -
163+
this.dim_element.top -
164+
this.dim_element.margin_top // prettier-ignore
165+
),
166+
right: Math.round(
167+
bounds.right +
168+
this.dim_element.right +
169+
this.dim_element.margin_right // prettier-ignore
170+
),
171+
bottom: Math.round(
172+
bounds.bottom +
173+
this.dim_element.bottom +
174+
this.dim_element.margin_bottom // prettier-ignore
175+
),
176+
left: Math.round(
177+
bounds.left -
178+
this.dim_element.left -
179+
this.dim_element.margin_left // prettier-ignore
180+
),
181+
};
182+
}
183+
184+
/**
185+
* Get the bumping state of the element.
186+
*
187+
* @returns {Object} The bumping state.
188+
*/
189+
get_bumping_state() {
190+
const pos_el = this._get_element_positions();
191+
const pos_x = this._get_container_positions(this.container_x, this.dim_container_x); // prettier-ignore
192+
const pos_y = this._get_container_positions(this.container_y, this.dim_container_y); // prettier-ignore
193+
194+
const bump_top = pos_el.top <= pos_y.top && pos_el.bottom >= pos_y.top;
195+
const bump_right = pos_el.right >= pos_x.right && pos_el.left <= pos_x.right;
196+
const bump_bottom = pos_el.bottom >= pos_y.bottom && pos_el.top <= pos_y.bottom;
197+
const bump_left = pos_el.left <= pos_x.left && pos_el.right >= pos_x.left;
198+
199+
const is_bumping = bump_top || bump_right || bump_bottom || bump_left;
200+
201+
return {
202+
bump_top,
203+
bump_right,
204+
bump_bottom,
205+
bump_left,
206+
is_bumping,
207+
};
208+
}
209+
210+
/**
211+
* Set the bumping classes on the element.
212+
*/
213+
set_bumping_classes() {
214+
const bumping_state = this.get_bumping_state();
215+
216+
const classes_to_add = [];
217+
const classes_to_remove = [];
218+
219+
if (bumping_state.is_bumping) {
220+
this.options.bump.add && classes_to_add.push(this.options.bump.add);
221+
this.options.bump.remove && classes_to_remove.push(this.options.bump.remove);
222+
223+
bumping_state.bump_top
224+
? classes_to_add.push("bumped-top")
225+
: classes_to_remove.push("bumped-top");
226+
bumping_state.bump_right
227+
? classes_to_add.push("bumped-right")
228+
: classes_to_remove.push("bumped-right");
229+
bumping_state.bump_bottom
230+
? classes_to_add.push("bumped-bottom")
231+
: classes_to_remove.push("bumped-bottom");
232+
bumping_state.bump_left
233+
? classes_to_add.push("bumped-left")
234+
: classes_to_remove.push("bumped-left");
235+
} else {
236+
this.options.unbump.add && classes_to_add.push(this.options.unbump.add);
237+
this.options.unbump.remove &&
238+
classes_to_remove.push(this.options.unbump.remove);
239+
classes_to_remove.push(
240+
"bumped-top",
241+
"bumped-right",
242+
"bumped-bottom",
243+
"bumped-left"
244+
);
132245
}
246+
247+
this.el.classList.remove(...classes_to_remove);
248+
this.el.classList.add(...classes_to_add);
133249
}
134250
}
135251

0 commit comments

Comments
 (0)