Skip to content

Commit fd68f4e

Browse files
authored
Merge pull request #796 from Patternslib/scr-840
pat-scroll: selector:bottom
2 parents 3ad3218 + e9d251f commit fd68f4e

File tree

5 files changed

+99
-48
lines changed

5 files changed

+99
-48
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
- pat inject: Rebase URLs in pattern configuration attributes. This avoids URLs in pattern configuration to point to unreachable paths in the context where the result is injected into.
6363
- pat forward: Add `delay` option for delaying the click action forwarding for a given number of milliseconds.
6464
- pat forward: Add `self` as possible value for the `selector` option to trigger the event on itself.
65+
- pat-scroll: Implement `selector:bottom` attribute value to scroll to the bottom of the scroll container.
6566

6667
### Technical
6768

src/pat/scroll/documentation.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,13 @@ Automatically scrolling once the page loads
4747
Scrolling can be configured through a `data-pat-scroll` attribute.
4848
The available options are:
4949

50-
| Field | Default | Options | Description |
51-
| ----------- | ------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
52-
| `trigger` | `click` | `click`, `auto` | `auto` means that the scrolling will happen as soon as the page loads. `click` means that the configured element needs to be clicked first. |
53-
| `direction` | `top` | `top`, `left` | The direction in which the scrolling happens. |
54-
| `selector` | `top`, CSS selector | A CSS or jQuery selector string or 'top'. | A selector for the element which will be scrolled by a number of pixels equal to `offset`. By default it will be the element on which the pattern is declared. Ignored unless `offset` is specified. |
55-
| `offset` | | A number | `offset` can only be used with scrollable elements. (An element is "scrollable" if it has scrollbars, i.e. when the CSS property `overflow` is either `auto` or `scroll`.) The element scrolled by `offset` can be specified with the `selector` option. If `selector` is not present, the element on which `pat-scroll` is declared will be scrolled. |
50+
| Field | Default | Options | Description |
51+
| ----------- | ----------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
52+
| `trigger` | `click` | `click`, `auto` | `auto` means that the scrolling will happen as soon as the page loads. `click` means that the configured element needs to be clicked first. |
53+
| `direction` | `top` | `top`, `left` | The direction in which the scrolling happens. |
54+
| `selector` | `top`, `bottom`, CSS selector | A CSS or jQuery selector string or 'top'. | A selector for the element which will be scrolled by a number of pixels equal to `offset`. By default it will be the element on which the pattern is declared. Ignored unless `offset` is specified. |
55+
| `offset` | | A number | `offset` can only be used with scrollable elements. (An element is "scrollable" if it has scrollbars, i.e. when the CSS property `overflow` is either `auto` or `scroll`.) The element scrolled by `offset` can be specified with the `selector` option. If `selector` is not present, the element on which `pat-scroll` is declared will be scrolled. |
56+
57+
58+
Note: Selector `top` will scroll the scroll container to the top, `bottom` to the bottom.
59+

src/pat/scroll/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,25 @@
1414
.current {
1515
background-color: beige;
1616
}
17+
#scrollable {
18+
overflow: scroll;
19+
max-height: 80vh;
20+
height: 40em;
21+
border: 2px solid blue;
22+
}
1723
</style>
1824
<body>
1925
<article style="height: 100px">
2026
<p>Some filler</p>
2127
</article>
2228
<article class="pat-rich" id="scrollable">
29+
<p>
30+
<button
31+
class="pat-scroll"
32+
data-pat-scroll="selector: bottom"
33+
>Scroll to bottom</button
34+
>
35+
</p>
2336
<ol class="mainnav" id="demo-main-nav">
2437
<li>
2538
<a href="#p1" class="pat-scroll"

src/pat/scroll/scroll.js

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,7 @@ export default Base.extend({
2727
ImagesLoaded = await import("imagesloaded");
2828
ImagesLoaded = ImagesLoaded.default;
2929
// Only calculate the offset when all images are loaded
30-
ImagesLoaded(
31-
$("body"),
32-
function () {
33-
this.smoothScroll();
34-
}.bind(this)
35-
);
30+
ImagesLoaded($("body"), () => this.smoothScroll());
3631
} else if (this.options.trigger == "click") {
3732
this.$el.click(this.onClick.bind(this));
3833
}
@@ -42,7 +37,7 @@ export default Base.extend({
4237
$(window).scroll(_.debounce(this.markIfVisible.bind(this), 50));
4338
},
4439

45-
onClick: function () {
40+
onClick() {
4641
//ev.preventDefault();
4742
history.pushState({}, null, this.$el.attr("href"));
4843
this.smoothScroll();
@@ -51,44 +46,43 @@ export default Base.extend({
5146
$("a.pat-scroll").trigger("hashchange");
5247
},
5348

54-
markBasedOnFragment: function () {
49+
markBasedOnFragment() {
5550
// Get the fragment from the URL and set the corresponding this.$el as current
5651
const fragment = window.location.hash.substr(1);
5752
if (fragment) {
58-
var $target = $("#" + fragment);
53+
const $target = $("#" + fragment);
5954
this.$el.addClass("current"); // the element that was clicked on
6055
$target.addClass("current");
6156
}
6257
},
6358

64-
clearIfHidden: function () {
65-
var active_target = "#" + window.location.hash.substr(1),
66-
$active_target = $(active_target),
67-
target = "#" + this.$el[0].href.split("#").pop();
59+
clearIfHidden() {
60+
const active_target = "#" + window.location.hash.substr(1);
61+
const $active_target = $(active_target);
62+
const target = "#" + this.$el[0].href.split("#").pop();
6863
if ($active_target.length > 0) {
6964
if (active_target != target) {
7065
// if the element does not match the one listed in the url #,
7166
// clear the current class from it.
72-
var $target = $("#" + this.$el[0].href.split("#").pop());
67+
const $target = $("#" + this.$el[0].href.split("#").pop());
7368
$target.removeClass("current");
7469
this.$el.removeClass("current");
7570
}
7671
}
7772
},
7873

79-
markIfVisible: function () {
80-
var fragment, $target, href;
74+
markIfVisible() {
8175
if (this.$el.hasClass("pat-scroll-animated")) {
8276
// this section is triggered when the scrolling is a result of the animate function
8377
// ie. automatic scrolling as opposed to the user manually scrolling
8478
this.$el.removeClass("pat-scroll-animated");
8579
} else if (this.$el[0].nodeName === "A") {
86-
href = this.$el[0].href;
87-
fragment =
80+
const href = this.$el[0].href;
81+
const fragment =
8882
(href.indexOf("#") !== -1 && href.split("#").pop()) ||
8983
undefined;
9084
if (fragment) {
91-
$target = $("#" + fragment);
85+
const $target = $("#" + fragment);
9286
if ($target.length) {
9387
if (
9488
utils.isElementInViewport(
@@ -108,19 +102,18 @@ export default Base.extend({
108102
}
109103
},
110104

111-
onPatternsUpdate: function (ev, data) {
112-
var fragment, $target, href;
105+
onPatternsUpdate(ev, data) {
113106
if (data.pattern === "stacks") {
114107
if (data.originalEvent && data.originalEvent.type === "click") {
115108
this.smoothScroll();
116109
}
117110
} else if (data.pattern === "scroll") {
118-
href = this.$el[0].href;
119-
fragment =
111+
const href = this.$el[0].href;
112+
const fragment =
120113
(href.indexOf("#") !== -1 && href.split("#").pop()) ||
121114
undefined;
122115
if (fragment) {
123-
$target = $("#" + fragment);
116+
const $target = $("#" + fragment);
124117
if ($target.length) {
125118
if (
126119
utils.isElementInViewport(
@@ -138,18 +131,18 @@ export default Base.extend({
138131
}
139132
},
140133

141-
findScrollContainer: function (el) {
142-
var direction = this.options.direction;
143-
var scrollable = $(el)
134+
findScrollContainer(el) {
135+
const direction = this.options.direction;
136+
let scrollable = $(el)
144137
.parents()
145-
.filter(function () {
138+
.filter((idx, el) => {
146139
return (
147-
["auto", "scroll"].indexOf($(this).css("overflow")) > -1 ||
140+
["auto", "scroll"].indexOf($(el).css("overflow")) > -1 ||
148141
(direction === "top" &&
149-
["auto", "scroll"].indexOf($(this).css("overflow-y")) >
142+
["auto", "scroll"].indexOf($(el).css("overflow-y")) >
150143
-1) ||
151144
(direction === "left" &&
152-
["auto", "scroll"].indexOf($(this).css("overflow-x")) >
145+
["auto", "scroll"].indexOf($(el).css("overflow-x")) >
153146
-1)
154147
);
155148
})
@@ -160,12 +153,11 @@ export default Base.extend({
160153
return scrollable;
161154
},
162155

163-
smoothScroll: function () {
164-
var href, fragment;
165-
var scroll =
166-
this.options.direction == "top" ? "scrollTop" : "scrollLeft",
167-
scrollable,
168-
options = {};
156+
smoothScroll() {
157+
const scroll =
158+
this.options.direction == "top" ? "scrollTop" : "scrollLeft";
159+
const options = {};
160+
let scrollable;
169161
if (typeof this.options.offset != "undefined") {
170162
// apply scroll options directly
171163
scrollable = this.options.selector
@@ -176,21 +168,30 @@ export default Base.extend({
176168
// Just scroll up or left, period.
177169
scrollable = this.findScrollContainer(this.$el);
178170
options[scroll] = 0;
171+
} else if (this.options.selector === "bottom") {
172+
// Just scroll down or right, period.
173+
scrollable = this.findScrollContainer(this.$el);
174+
if (scroll === "scrollTop") {
175+
options.scrollTop = scrollable[0].scrollHeight;
176+
} else {
177+
options.scrollLeft = scrollable[0].scrollWidth;
178+
}
179179
} else {
180180
// Get the first element with overflow (the scroll container)
181181
// starting from the *target*
182182
// The intent is to move target into view within scrollable
183183
// if the scrollable has no scrollbar, do not scroll body
184+
let fragment;
184185
if (this.options.selector) {
185186
fragment = this.options.selector;
186187
} else {
187-
href = this.$el.attr("href");
188+
const href = this.$el.attr("href");
188189
fragment =
189190
href.indexOf("#") !== -1
190191
? "#" + href.split("#").pop()
191192
: undefined;
192193
}
193-
var target = $(fragment);
194+
const target = $(fragment);
194195
if (target.length === 0) {
195196
return;
196197
}
@@ -223,9 +224,7 @@ export default Base.extend({
223224
// execute the scroll
224225
scrollable.animate(options, {
225226
duration: 500,
226-
start: function () {
227-
$(".pat-scroll").addClass("pat-scroll-animated");
228-
},
227+
start: () => $(".pat-scroll").addClass("pat-scroll-animated"),
229228
});
230229
},
231230
});

src/pat/scroll/scroll.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,39 @@ describe("pat-scroll", function () {
6666
expect(spy_animate).toHaveBeenCalled();
6767
done();
6868
});
69+
70+
// Skipping - passes only in isolation.
71+
it.skip("will scroll to bottom with selector:bottom", async function (done) {
72+
const outer = document.createElement("div");
73+
outer.innerHTML = `
74+
<div id="scroll-container" style="overflow: scroll">
75+
<button class="pat-scroll" data-pat-scroll="selector: bottom">to bottom</button>
76+
</div>
77+
`;
78+
79+
const container = outer.querySelector("#scroll-container");
80+
const trigger = outer.querySelector(".pat-scroll");
81+
82+
// mocking stuff jsDOM doesn't implement
83+
jest.spyOn(container, "scrollHeight", "get").mockImplementation(
84+
() => 100000
85+
);
86+
87+
expect(container.scrollTop).toBe(0);
88+
89+
pattern.init(trigger);
90+
await utils.timeout(1); // wait a tick for async to settle.
91+
92+
expect(container.scrollTop).toBe(0);
93+
94+
trigger.click();
95+
await utils.timeout(1); // wait a tick for async to settle.
96+
97+
expect(container.scrollTop > 0).toBe(true);
98+
99+
jest.restoreAllMocks();
100+
101+
done();
102+
});
69103
});
70104
});

0 commit comments

Comments
 (0)