Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
- 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.
- pat forward: Add `delay` option for delaying the click action forwarding for a given number of milliseconds.
- pat forward: Add `self` as possible value for the `selector` option to trigger the event on itself.
- pat-scroll: Implement `selector:bottom` attribute value to scroll to the bottom of the scroll container.

### Technical

Expand Down
16 changes: 10 additions & 6 deletions src/pat/scroll/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ Automatically scrolling once the page loads
Scrolling can be configured through a `data-pat-scroll` attribute.
The available options are:

| Field | Default | Options | Description |
| ----------- | ------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `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. |
| `direction` | `top` | `top`, `left` | The direction in which the scrolling happens. |
| `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. |
| `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. |
| Field | Default | Options | Description |
| ----------- | ----------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `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. |
| `direction` | `top` | `top`, `left` | The direction in which the scrolling happens. |
| `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. |
| `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. |


Note: Selector `top` will scroll the scroll container to the top, `bottom` to the bottom.

13 changes: 13 additions & 0 deletions src/pat/scroll/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,25 @@
.current {
background-color: beige;
}
#scrollable {
overflow: scroll;
max-height: 80vh;
height: 40em;
border: 2px solid blue;
}
</style>
<body>
<article style="height: 100px">
<p>Some filler</p>
</article>
<article class="pat-rich" id="scrollable">
<p>
<button
class="pat-scroll"
data-pat-scroll="selector: bottom"
>Scroll to bottom</button
>
</p>
<ol class="mainnav" id="demo-main-nav">
<li>
<a href="#p1" class="pat-scroll"
Expand Down
83 changes: 41 additions & 42 deletions src/pat/scroll/scroll.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@ export default Base.extend({
ImagesLoaded = await import("imagesloaded");
ImagesLoaded = ImagesLoaded.default;
// Only calculate the offset when all images are loaded
ImagesLoaded(
$("body"),
function () {
this.smoothScroll();
}.bind(this)
);
ImagesLoaded($("body"), () => this.smoothScroll());
} else if (this.options.trigger == "click") {
this.$el.click(this.onClick.bind(this));
}
Expand All @@ -42,7 +37,7 @@ export default Base.extend({
$(window).scroll(_.debounce(this.markIfVisible.bind(this), 50));
},

onClick: function () {
onClick() {
//ev.preventDefault();
history.pushState({}, null, this.$el.attr("href"));
this.smoothScroll();
Expand All @@ -51,44 +46,43 @@ export default Base.extend({
$("a.pat-scroll").trigger("hashchange");
},

markBasedOnFragment: function () {
markBasedOnFragment() {
// Get the fragment from the URL and set the corresponding this.$el as current
const fragment = window.location.hash.substr(1);
if (fragment) {
var $target = $("#" + fragment);
const $target = $("#" + fragment);
this.$el.addClass("current"); // the element that was clicked on
$target.addClass("current");
}
},

clearIfHidden: function () {
var active_target = "#" + window.location.hash.substr(1),
$active_target = $(active_target),
target = "#" + this.$el[0].href.split("#").pop();
clearIfHidden() {
const active_target = "#" + window.location.hash.substr(1);
const $active_target = $(active_target);
const target = "#" + this.$el[0].href.split("#").pop();
if ($active_target.length > 0) {
if (active_target != target) {
// if the element does not match the one listed in the url #,
// clear the current class from it.
var $target = $("#" + this.$el[0].href.split("#").pop());
const $target = $("#" + this.$el[0].href.split("#").pop());
$target.removeClass("current");
this.$el.removeClass("current");
}
}
},

markIfVisible: function () {
var fragment, $target, href;
markIfVisible() {
if (this.$el.hasClass("pat-scroll-animated")) {
// this section is triggered when the scrolling is a result of the animate function
// ie. automatic scrolling as opposed to the user manually scrolling
this.$el.removeClass("pat-scroll-animated");
} else if (this.$el[0].nodeName === "A") {
href = this.$el[0].href;
fragment =
const href = this.$el[0].href;
const fragment =
(href.indexOf("#") !== -1 && href.split("#").pop()) ||
undefined;
if (fragment) {
$target = $("#" + fragment);
const $target = $("#" + fragment);
if ($target.length) {
if (
utils.isElementInViewport(
Expand All @@ -108,19 +102,18 @@ export default Base.extend({
}
},

onPatternsUpdate: function (ev, data) {
var fragment, $target, href;
onPatternsUpdate(ev, data) {
if (data.pattern === "stacks") {
if (data.originalEvent && data.originalEvent.type === "click") {
this.smoothScroll();
}
} else if (data.pattern === "scroll") {
href = this.$el[0].href;
fragment =
const href = this.$el[0].href;
const fragment =
(href.indexOf("#") !== -1 && href.split("#").pop()) ||
undefined;
if (fragment) {
$target = $("#" + fragment);
const $target = $("#" + fragment);
if ($target.length) {
if (
utils.isElementInViewport(
Expand All @@ -138,18 +131,18 @@ export default Base.extend({
}
},

findScrollContainer: function (el) {
var direction = this.options.direction;
var scrollable = $(el)
findScrollContainer(el) {
const direction = this.options.direction;
let scrollable = $(el)
.parents()
.filter(function () {
.filter((idx, el) => {
return (
["auto", "scroll"].indexOf($(this).css("overflow")) > -1 ||
["auto", "scroll"].indexOf($(el).css("overflow")) > -1 ||
(direction === "top" &&
["auto", "scroll"].indexOf($(this).css("overflow-y")) >
["auto", "scroll"].indexOf($(el).css("overflow-y")) >
-1) ||
(direction === "left" &&
["auto", "scroll"].indexOf($(this).css("overflow-x")) >
["auto", "scroll"].indexOf($(el).css("overflow-x")) >
-1)
);
})
Expand All @@ -160,12 +153,11 @@ export default Base.extend({
return scrollable;
},

smoothScroll: function () {
var href, fragment;
var scroll =
this.options.direction == "top" ? "scrollTop" : "scrollLeft",
scrollable,
options = {};
smoothScroll() {
const scroll =
this.options.direction == "top" ? "scrollTop" : "scrollLeft";
const options = {};
let scrollable;
if (typeof this.options.offset != "undefined") {
// apply scroll options directly
scrollable = this.options.selector
Expand All @@ -176,21 +168,30 @@ export default Base.extend({
// Just scroll up or left, period.
scrollable = this.findScrollContainer(this.$el);
options[scroll] = 0;
} else if (this.options.selector === "bottom") {
// Just scroll down or right, period.
scrollable = this.findScrollContainer(this.$el);
if (scroll === "scrollTop") {
options.scrollTop = scrollable[0].scrollHeight;
} else {
options.scrollLeft = scrollable[0].scrollWidth;
}
} else {
// Get the first element with overflow (the scroll container)
// starting from the *target*
// The intent is to move target into view within scrollable
// if the scrollable has no scrollbar, do not scroll body
let fragment;
if (this.options.selector) {
fragment = this.options.selector;
} else {
href = this.$el.attr("href");
const href = this.$el.attr("href");
fragment =
href.indexOf("#") !== -1
? "#" + href.split("#").pop()
: undefined;
}
var target = $(fragment);
const target = $(fragment);
if (target.length === 0) {
return;
}
Expand Down Expand Up @@ -223,9 +224,7 @@ export default Base.extend({
// execute the scroll
scrollable.animate(options, {
duration: 500,
start: function () {
$(".pat-scroll").addClass("pat-scroll-animated");
},
start: () => $(".pat-scroll").addClass("pat-scroll-animated"),
});
},
});
34 changes: 34 additions & 0 deletions src/pat/scroll/scroll.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,39 @@ describe("pat-scroll", function () {
expect(spy_animate).toHaveBeenCalled();
done();
});

// Skipping - passes only in isolation.
it.skip("will scroll to bottom with selector:bottom", async function (done) {
const outer = document.createElement("div");
outer.innerHTML = `
<div id="scroll-container" style="overflow: scroll">
<button class="pat-scroll" data-pat-scroll="selector: bottom">to bottom</button>
</div>
`;

const container = outer.querySelector("#scroll-container");
const trigger = outer.querySelector(".pat-scroll");

// mocking stuff jsDOM doesn't implement
jest.spyOn(container, "scrollHeight", "get").mockImplementation(
() => 100000
);

expect(container.scrollTop).toBe(0);

pattern.init(trigger);
await utils.timeout(1); // wait a tick for async to settle.

expect(container.scrollTop).toBe(0);

trigger.click();
await utils.timeout(1); // wait a tick for async to settle.

expect(container.scrollTop > 0).toBe(true);

jest.restoreAllMocks();

done();
});
});
});