Skip to content

Commit 9a0f7a3

Browse files
committed
feat(pat navigation): Add URL-based navigation markers.
That feature was also present in the old implementation but is now improved.
1 parent e9da003 commit 9a0f7a3

File tree

2 files changed

+234
-2
lines changed

2 files changed

+234
-2
lines changed

src/pat/navigation/navigation.js

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ export default Base.extend({
1818

1919
this.init_listeners();
2020

21-
this.mark_current();
21+
if (this.el.querySelector(this.options.currentClass)) {
22+
log.debug("Mark navigation items based on existing current class");
23+
this.mark_current();
24+
} else {
25+
log.debug("Mark navigation items based on URL pattern.");
26+
this.mark_items_url();
27+
}
2228
},
2329

2430
/**
@@ -50,7 +56,14 @@ export default Base.extend({
5056
// Re-init when navigation changes.
5157
const observer = new MutationObserver(() => {
5258
this.init_listeners();
53-
this.mark_current();
59+
60+
if (this.el.querySelector(this.options.currentClass)) {
61+
log.debug("Mark navigation items based on existing current class");
62+
this.mark_current();
63+
} else {
64+
log.debug("Mark navigation items based on URL pattern.");
65+
this.mark_items_url();
66+
}
5467
});
5568
observer.observe(this.el, {
5669
childList: true,
@@ -123,6 +136,59 @@ export default Base.extend({
123136
}
124137
},
125138

139+
/**
140+
* Mark all navigation items that are in the path of the current url.
141+
*
142+
* @param {String} [url] - The url to check against.
143+
* If not given, the current url will be used.
144+
*/
145+
mark_items_url(url) {
146+
const current_url = url || this.base_url();
147+
const current_url_prepared = this.prepare_url(current_url);
148+
149+
const portal_url = this.prepare_url(document.body.dataset?.portalUrl);
150+
const nav_items = this.el.querySelectorAll("a");
151+
152+
for (const nav_item of nav_items) {
153+
// Get the nav item's url and rebase it against the current url to
154+
// make absolute or relative URLs FQDN URLs.
155+
const nav_url = this.prepare_url(
156+
new URL(nav_item.getAttribute("href", ""), current_url)?.href
157+
);
158+
159+
const wrapper = this.options.itemWrapper
160+
? nav_item.closest(this.options.itemWrapper)
161+
: nav_item.parentNode;
162+
163+
if (nav_url === current_url_prepared) {
164+
nav_item.classList.add(this.options.currentClass);
165+
wrapper.classList.add(this.options.currentClass);
166+
} else if (
167+
// Compare the current navigation item url with a slash at the
168+
// end - if it is "inPath" it must have a slash in it.
169+
current_url_prepared.indexOf(`${nav_url}/`) === 0 &&
170+
// Do not set inPath for the "Home" url, as this would always
171+
// be in the path.
172+
nav_url !== portal_url
173+
) {
174+
nav_item.classList.add(this.options.inPathClass);
175+
wrapper.classList.add(this.options.inPathClass);
176+
} else {
177+
// Not even in path.
178+
continue;
179+
}
180+
181+
// The path was at least found in the current url, so we need to
182+
// check the input-openers within the path
183+
// Find the first input which is the correct one, even if this
184+
// navigation item has many children.
185+
// These hidden checkboxes are used to open the navigation item for
186+
// mobile navigation.
187+
const check = wrapper.querySelector("input");
188+
if (check) check.checked = true;
189+
}
190+
},
191+
126192
/**
127193
* Clear all navigation items from the inPath and current classes
128194
*/
@@ -135,4 +201,31 @@ export default Base.extend({
135201
item.classList.remove(this.options.currentClass);
136202
}
137203
},
204+
205+
/**
206+
* Prepare a URL for comparison.
207+
* Plone-specific "/view" and "@@" will be removed as well as a trailing slash.
208+
*
209+
* @param {String} url - The url to prepare.
210+
*
211+
* @returns {String} - The prepared url.
212+
*/
213+
prepare_url(url) {
214+
return url?.replace("/view", "").replaceAll("@@", "").replace(/\/$/, "");
215+
},
216+
217+
/**
218+
* Get the URL of the current page.
219+
* If a ``canonical`` meta tag is found, return this.
220+
* Otherwise return the window.location URL.
221+
* Already prepare the URL for comparison.
222+
*
223+
* @returns {String} - The current URL.
224+
*/
225+
base_url() {
226+
return this.prepare_url(
227+
document.querySelector('head link[rel="canonical"]')?.href ||
228+
window.location.href
229+
);
230+
},
138231
});

src/pat/navigation/navigation.test.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,142 @@ describe("Navigation pattern tests - no predefined structure", function () {
200200
expect(a11.classList.contains("navigation-in-path")).toBeFalsy();
201201
});
202202
});
203+
204+
describe("Navigation pattern tests - Mark items based on URL", () => {
205+
let _window_location;
206+
207+
beforeEach(() => {
208+
_window_location = global.window.location;
209+
delete global.window.location;
210+
document.body.innerHTML = "";
211+
});
212+
213+
afterEach(() => {
214+
global.window.location = _window_location;
215+
});
216+
217+
const set_url = (url, portal_url) => {
218+
global.window.location = {
219+
href: url,
220+
};
221+
222+
portal_url = portal_url || url;
223+
224+
document.body.dataset.portalUrl = portal_url;
225+
};
226+
227+
it("navigation roundtrip", () => {
228+
document.body.innerHTML = `
229+
<nav class="pat-navigation"
230+
data-pat-navigation="in-path-class: inPath">
231+
<ul>
232+
<li>
233+
<a href="/">Home</a>
234+
</li>
235+
<li>
236+
<a href="/path1">p1</a>
237+
</li>
238+
<li>
239+
<a href="/path2">p2</a>
240+
<ul>
241+
<li>
242+
<a href="/path2/path2.1">p2.1</a>
243+
</li>
244+
<li>
245+
<a href="/path2/path2.2">p2.2</a>
246+
<ul>
247+
<li>
248+
<a href="/path2/path2.2/path2.2.1">p2.2.1</a>
249+
</li>
250+
<li>
251+
<a href="/path2/path2.2/path2.2.2">p2.2.2</a>
252+
</li>
253+
</ul>
254+
</li>
255+
</ul>
256+
</li>
257+
<li>
258+
<a href="../../path3">p1</a>
259+
</li>
260+
<li>
261+
<a href="https://patternslib.com/path4">p1</a>
262+
</li>
263+
</ul>
264+
</nav>
265+
`;
266+
267+
set_url("https://patternslib.com/");
268+
269+
const instance = new Pattern(document.querySelector(".pat-navigation"));
270+
271+
const it0 = document.querySelector("a[href='/']");
272+
const it1 = document.querySelector("a[href='/path1']");
273+
const it2 = document.querySelector("a[href='/path2']");
274+
const it21 = document.querySelector("a[href='/path2/path2.1']");
275+
const it22 = document.querySelector("a[href='/path2/path2.2']");
276+
const it221 = document.querySelector("a[href='/path2/path2.2/path2.2.1']");
277+
const it222 = document.querySelector("a[href='/path2/path2.2/path2.2.2']");
278+
const it3 = document.querySelector("a[href='../../path3']");
279+
const it4 = document.querySelector("a[href='https://patternslib.com/path4']");
280+
281+
expect(document.querySelectorAll(".current").length).toBe(2);
282+
expect(document.querySelectorAll(".inPath").length).toBe(0);
283+
expect(document.querySelector(".current a")).toBe(it0);
284+
285+
instance.clear_items();
286+
instance.mark_items_url("https://patternslib.com/path1");
287+
288+
expect(document.querySelectorAll(".current").length).toBe(2);
289+
expect(document.querySelectorAll(".inPath").length).toBe(0);
290+
expect(document.querySelector(".current a")).toBe(it1);
291+
292+
instance.clear_items();
293+
instance.mark_items_url("https://patternslib.com/path2");
294+
295+
expect(document.querySelectorAll(".current").length).toBe(2);
296+
expect(document.querySelectorAll(".inPath").length).toBe(0);
297+
expect(document.querySelector(".current a")).toBe(it2);
298+
299+
instance.clear_items();
300+
instance.mark_items_url("https://patternslib.com/path2/path2.1");
301+
302+
expect(document.querySelectorAll(".current").length).toBe(2);
303+
expect(document.querySelectorAll(".inPath").length).toBe(2);
304+
expect(document.querySelector(".current a")).toBe(it21);
305+
306+
instance.clear_items();
307+
instance.mark_items_url("https://patternslib.com/path2/path2.2");
308+
309+
expect(document.querySelectorAll(".current").length).toBe(2);
310+
expect(document.querySelectorAll(".inPath").length).toBe(2);
311+
expect(document.querySelector(".current a")).toBe(it22);
312+
313+
instance.clear_items();
314+
instance.mark_items_url("https://patternslib.com/path2/path2.2/path2.2.1");
315+
316+
expect(document.querySelectorAll(".current").length).toBe(2);
317+
expect(document.querySelectorAll(".inPath").length).toBe(4);
318+
expect(document.querySelector(".current a")).toBe(it221);
319+
320+
instance.clear_items();
321+
instance.mark_items_url("https://patternslib.com/path2/path2.2/path2.2.2");
322+
323+
expect(document.querySelectorAll(".current").length).toBe(2);
324+
expect(document.querySelectorAll(".inPath").length).toBe(4);
325+
expect(document.querySelector(".current a")).toBe(it222);
326+
327+
instance.clear_items();
328+
instance.mark_items_url("https://patternslib.com/path3");
329+
330+
expect(document.querySelectorAll(".current").length).toBe(2);
331+
expect(document.querySelectorAll(".inPath").length).toBe(0);
332+
expect(document.querySelector(".current a")).toBe(it3);
333+
334+
instance.clear_items();
335+
instance.mark_items_url("https://patternslib.com/path4");
336+
337+
expect(document.querySelectorAll(".current").length).toBe(2);
338+
expect(document.querySelectorAll(".inPath").length).toBe(0);
339+
expect(document.querySelector(".current a")).toBe(it4);
340+
});
341+
});

0 commit comments

Comments
 (0)