Skip to content

Commit 3cf86cc

Browse files
authored
Merge pull request #1143 from Patternslib/scrum-1106--scrollspy
Add scrollspy functionality to pat-navigation.
2 parents e9a6d61 + fb8eb82 commit 3cf86cc

20 files changed

+1284
-131
lines changed

src/core/base.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ const Base = async function ($el, options, trigger) {
5656
this.$el = $el;
5757
this.el = $el[0];
5858
this.options = $.extend(true, {}, this.defaults || {}, options || {});
59+
60+
this.emit("pre-init");
61+
5962
await this.init($el, options, trigger);
6063

6164
// Store pattern instance on element

src/core/base.test.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,15 +219,21 @@ describe("pat-base: The Base class for patterns", function () {
219219
const node = document.createElement("div");
220220
node.setAttribute("class", "pat-example");
221221
const event_list = [];
222-
node.addEventListener("init_done", () => event_list.push("pat init"));
223-
$(node).on("init.example.patterns", () => event_list.push("base init"));
222+
$(node).on("pre-init.example.patterns", () =>
223+
event_list.push("pre-init.example.patterns")
224+
);
225+
node.addEventListener("init_done", () => event_list.push("init_done"));
226+
$(node).on("init.example.patterns", () =>
227+
event_list.push("init.example.patterns")
228+
);
224229
new Tmp(node);
225230

226231
// await until all asyncs are settled. 1 event loop should be enough.
227232
await utils.timeout(1);
228233

229-
expect(event_list[0]).toBe("pat init");
230-
expect(event_list[1]).toBe("base init");
234+
expect(event_list[0]).toBe("pre-init.example.patterns");
235+
expect(event_list[1]).toBe("init_done");
236+
expect(event_list[2]).toBe("init.example.patterns");
231237
});
232238

233239
it("adds the pattern instance on the element when manually initialized", async () => {

src/core/basepattern.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ class BasePattern {
1616
static trigger; // A CSS selector to match elements that should trigger the pattern instantiation.
1717
static parser; // Options parser.
1818

19+
// Parser options
20+
parser_group_options = true;
21+
parser_multiple = undefined;
22+
parser_inherit = true;
23+
1924
constructor(el, options = {}) {
2025
// Make static variables available on instance.
2126
this.name = this.constructor.name;
@@ -31,6 +36,14 @@ class BasePattern {
3136
}
3237
this.el = el;
3338

39+
// Notify pre-init
40+
this.el.dispatchEvent(
41+
new Event(`pre-init.${this.name}.patterns`, {
42+
bubbles: true,
43+
cancelable: true,
44+
})
45+
);
46+
3447
// Initialize asynchronously.
3548
//
3649
// 1) We need to call the concrete implementation of ``init``, but the
@@ -60,7 +73,14 @@ class BasePattern {
6073

6174
// Create the options object by parsing the element and using the
6275
// optional options as default.
63-
this.options = this.parser?.parse(this.el, options) ?? options;
76+
this.options =
77+
this.parser?.parse(
78+
this.el,
79+
options,
80+
this.parser_multiple,
81+
this.parser_inherit,
82+
this.parser_group_options
83+
) ?? options;
6484

6585
// Store pattern instance on element
6686
this.el[`pattern-${this.name}`] = this;

src/core/basepattern.test.js

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe("Basepattern class tests", function () {
1515
jest.restoreAllMocks();
1616
});
1717

18-
it("1 - Trigger, name and parser are statically available on the class.", async function () {
18+
it("1.1 - Trigger, name and parser are statically available on the class.", async function () {
1919
class Pat extends BasePattern {
2020
static name = "example";
2121
static trigger = ".example";
@@ -37,6 +37,57 @@ describe("Basepattern class tests", function () {
3737
expect(typeof pat.parser.parse).toBe("function");
3838
});
3939

40+
it("1.2 - Options are created with grouping per default.", async function () {
41+
const Parser = (await import("./parser")).default;
42+
43+
const parser = new Parser("example");
44+
parser.addArgument("a", 1);
45+
parser.addArgument("camel-b", 2);
46+
parser.addArgument("test-a", 3);
47+
parser.addArgument("test-b", 4);
48+
49+
class Pat extends BasePattern {
50+
static name = "example";
51+
static trigger = ".example";
52+
static parser = parser;
53+
}
54+
55+
const el = document.createElement("div");
56+
const pat = new Pat(el);
57+
await utils.timeout(1);
58+
59+
expect(pat.options.a).toBe(1);
60+
expect(pat.options.camelB).toBe(2);
61+
expect(pat.options.test.a).toBe(3);
62+
expect(pat.options.test.b).toBe(4);
63+
});
64+
65+
it("1.3 - Option grouping can be turned off.", async function () {
66+
const Parser = (await import("./parser")).default;
67+
68+
const parser = new Parser("example");
69+
parser.addArgument("a", 1);
70+
parser.addArgument("camel-b", 2);
71+
parser.addArgument("test-a", 3);
72+
parser.addArgument("test-b", 4);
73+
74+
class Pat extends BasePattern {
75+
static name = "example";
76+
static trigger = ".example";
77+
static parser = parser;
78+
79+
parser_group_options = false;
80+
}
81+
82+
const el = document.createElement("div");
83+
const pat = new Pat(el);
84+
await utils.timeout(1);
85+
86+
expect(pat.options.a).toBe(1);
87+
expect(pat.options["camel-b"]).toBe(2);
88+
expect(pat.options["test-a"]).toBe(3);
89+
expect(pat.options["test-b"]).toBe(4);
90+
});
4091
it("2 - Base pattern is class based and does inheritance, polymorphism, encapsulation, ... pt1", async function () {
4192
class Pat1 extends BasePattern {
4293
some = "thing";
@@ -210,20 +261,37 @@ describe("Basepattern class tests", function () {
210261
expect(cnt).toBe(1);
211262
});
212263

213-
it("6.2 - Throws a init event after asynchronous initialization has finished.", async function () {
264+
it("6.2 - Throws bubbling initialization events.", async function () {
214265
const events = (await import("./events")).default;
215266
class Pat extends BasePattern {
216267
static name = "example";
217268
static trigger = ".example";
269+
270+
async init() {
271+
this.el.dispatchEvent(new Event("initializing"), { bubbles: true });
272+
}
218273
}
219274

220-
const el = document.createElement("div");
275+
document.body.innerHTML = "<div></div>";
276+
const el = document.querySelector("div");
277+
278+
const event_list = [];
279+
document.body.addEventListener("pre-init.example.patterns", () =>
280+
event_list.push("pre-init.example.patterns")
281+
);
282+
document.body.addEventListener("pre-init.example.patterns", () =>
283+
event_list.push("initializing")
284+
);
285+
document.body.addEventListener("pre-init.example.patterns", () =>
286+
event_list.push("init.example.patterns")
287+
);
221288

222289
const pat = new Pat(el);
223290
await events.await_pattern_init(pat);
224291

225-
// If test reaches this expect statement, the init event catched.
226-
expect(true).toBe(true);
292+
expect(event_list[0]).toBe("pre-init.example.patterns");
293+
expect(event_list[1]).toBe("initializing");
294+
expect(event_list[2]).toBe("init.example.patterns");
227295
});
228296

229297
it("6.3 - Throws a not-init event in case of an double initialization event which is handled by await_pattern_init.", async function () {

src/core/dom.js

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ const get_parents = (el) => {
169169
/**
170170
* Return the value of the first attribute found in the list of parents.
171171
*
172-
* @param {DOM element} el - The DOM element to start the acquisition search for the given attribute.
172+
* @param {Node} el - The DOM element to start the acquisition search for the given attribute.
173173
* @param {string} attribute - Name of the attribute to search for.
174174
* @param {Boolean} include_empty - Also return empty values.
175175
* @param {Boolean} include_all - Return a list of attribute values found in all parents.
@@ -245,7 +245,7 @@ function get_css_value(el, property, as_pixels = false, as_float = false) {
245245
* @param {String} [direction=] - Not given: Search for any scrollable element up in the DOM tree.
246246
* ``x``: Search for a horizontally scrollable element.
247247
* ``y``: Search for a vertically scrollable element.
248-
* @param {(DOM Node|null)} [fallback=document.body] - Fallback, if no scroll container can be found.
248+
* @param {(Node|null)} [fallback=document.body] - Fallback, if no scroll container can be found.
249249
* The default is to use document.body.
250250
*
251251
* @returns {Node} - Return the first scrollable element.
@@ -270,6 +270,38 @@ const find_scroll_container = (el, direction, fallback = document.body) => {
270270
return fallback;
271271
};
272272

273+
/**
274+
* Get the horizontal scroll position.
275+
*
276+
* @param {Node} scroll_reference - The element to get the scroll position from.
277+
*
278+
* @returns {number} The horizontal scroll position.
279+
*/
280+
const get_scroll_x = (scroll_reference) => {
281+
// scroll_listener == window: window.scrollX
282+
// scroll_listener == html: html.scrollLeft == window.scrollX
283+
// scroll_listener == DOM node: node.scrollLeft
284+
return typeof scroll_reference.scrollLeft !== "undefined"
285+
? scroll_reference.scrollLeft
286+
: scroll_reference.scrollX;
287+
};
288+
289+
/**
290+
* Get the vertical scroll position.
291+
*
292+
* @param {Node} scroll_reference - The element to get the scroll position from.
293+
*
294+
* @returns {number} The vertical scroll position.
295+
*/
296+
const get_scroll_y = (scroll_reference) => {
297+
// scroll_listener == window: window.scrollY
298+
// scroll_listener == html: html.scrollTop == window.scrollY
299+
// scroll_listener == DOM node: node.scrollTop
300+
return typeof scroll_reference.scrollTop !== "undefined"
301+
? scroll_reference.scrollTop
302+
: scroll_reference.scrollY;
303+
};
304+
273305
/**
274306
* Get data stored directly on the node instance.
275307
* We are using a prefix to make sure the data doesn't collide with other attributes.
@@ -336,6 +368,44 @@ const template = (template_string, template_variables = {}) => {
336368
return new Function("return `" + template_string + "`;").call(template_variables);
337369
};
338370

371+
/**
372+
* Get the visible ratio of an element compared to container.
373+
* If no container is given, the viewport is used.
374+
*
375+
* Note: currently only vertical ratio is supported.
376+
*
377+
* @param {Node} el - The element to get the visible ratio from.
378+
* @param {Node} [container] - The container to compare the element to.
379+
* @returns {number} - The visible ratio of the element.
380+
* 0 means the element is not visible.
381+
* 1 means the element is fully visible.
382+
*/
383+
const get_visible_ratio = (el, container) => {
384+
if (!el) {
385+
return 0;
386+
}
387+
388+
const rect = el.getBoundingClientRect();
389+
const container_rect =
390+
container !== window
391+
? container.getBoundingClientRect()
392+
: {
393+
top: 0,
394+
bottom: window.innerHeight,
395+
};
396+
397+
let visible_ratio = 0;
398+
if (rect.top < container_rect.bottom && rect.bottom > container_rect.top) {
399+
const rect_height = rect.bottom - rect.top;
400+
const visible_height =
401+
Math.min(rect.bottom, container_rect.bottom) -
402+
Math.max(rect.top, container_rect.top);
403+
visible_ratio = visible_height / rect_height;
404+
}
405+
406+
return visible_ratio;
407+
};
408+
339409
const dom = {
340410
toNodeArray: toNodeArray,
341411
querySelectorAllAndMe: querySelectorAllAndMe,
@@ -351,10 +421,13 @@ const dom = {
351421
create_from_string: create_from_string,
352422
get_css_value: get_css_value,
353423
find_scroll_container: find_scroll_container,
424+
get_scroll_x: get_scroll_x,
425+
get_scroll_y: get_scroll_y,
354426
get_data: get_data,
355427
set_data: set_data,
356428
delete_data: delete_data,
357429
template: template,
430+
get_visible_ratio: get_visible_ratio,
358431
add_event_listener: events.add_event_listener, // BBB export. TODO: Remove in an upcoming version.
359432
remove_event_listener: events.remove_event_listener, // BBB export. TODO: Remove in an upcoming version.
360433
};

0 commit comments

Comments
 (0)