Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b283a86
tech(pat-navigationmarker): Add tests for the navigationmarker.
thet Mar 16, 2026
0d977f3
maint(webpack): Set watcher dir to src.
thet Oct 25, 2024
a28deb8
feat(pat-structure): Support for pushStage, baseUrl.
thet Oct 25, 2024
ca927a8
breaking(pat-toolbar): Change to a class based Pattern. Should do no …
thet Jan 3, 2024
0e94fad
feat(pat-toolbar): Optionally import CSS.
thet Jan 3, 2024
9947501
feat(pat-toolbar): Allow to customize via options.
thet Jan 3, 2024
6b826f5
maint(pat-toolbar): Modernize code.
thet Jan 3, 2024
fb0b8b1
maint(pat-toolbar): Restructure for better reusability.
thet Jan 3, 2024
128945a
feat(pat-toolbar): After toolbar, also initialize patterns on persona…
thet Jan 3, 2024
3c115d6
feat(pat-toolbar): Dispatch event on toolbar after reloading.
thet Jan 4, 2024
8bbe981
feat(pat-toolbar): Reload itself on location change.
thet Oct 25, 2024
a13cee7
fix(pat-toolbar): Improve toolbar reload method to not reload unneces…
thet Oct 25, 2024
5295133
maint(pat-toolbar): Cleanup.
thet Dec 10, 2024
7c078a9
feat(pat-navigationmarker): Modernize: class based Pattern, add optio…
thet Sep 25, 2024
ec648bc
feat(pat-navigationmarker): re-scan nav after navigate event / url ch…
thet Sep 25, 2024
71e1c8f
feat(pat-navigationmarker): fire "pat-navigationmarker.scanned" event…
thet Sep 25, 2024
d84f91f
feat(pat-navigationmarker): multiple things: details support, BBB bre…
thet Sep 27, 2024
26fc43c
feat(pat-navigationmarker): Also mark the anchor itself as current.
thet Sep 27, 2024
3151979
fix(pat-navigationmarker): link-canonical isn't always updated. just …
thet Aug 26, 2025
2082814
yarn install.
thet Sep 2, 2025
00915d0
fixup structure.
thet Oct 10, 2025
ebd6adf
feat(pat-base-url): Add new pattern to update data-base-url on the bo…
thet Oct 14, 2025
2ff136e
fixup pat-toolbar: Remove the render URL cleanup method. This is now …
thet Oct 14, 2025
b8df4f9
fixup pat-toolbar: Rename render-url to render-view, as URL was seman…
thet Oct 14, 2025
9859668
fixup pat-structure: No need to set the base URL. Using history.pushS…
thet Oct 14, 2025
535fbb7
pat-navigationmarker: autoformat.
thet Mar 16, 2026
7cfc647
pat-navigationmarker: Use the URL information from the navigate event…
thet Mar 16, 2026
d5383d6
pat-navigationmarker: TODO TODO: define TODOs.
thet Mar 16, 2026
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@11ty/eleventy-upgrade-help": "3.0.2",
"@patternslib/pat-code-editor": "4.0.1",
"@patternslib/patternslib": "9.10.4",
"@plone/plonetheme-barceloneta-base": "^3.3.0",
"@plone/registry": "^2.7.0",
"backbone": "1.6.1",
"backbone.paginator": "2.0.8",
Expand Down
4 changes: 4 additions & 0 deletions src/pat/base-url/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# pat-base-url

Update the `data-base-url` attribute on the body tag when a `navigate` event is
thrown and thus the browser's URL has changed.
49 changes: 49 additions & 0 deletions src/pat/base-url/base-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Set the base URL based on the current location, listening on navigation changes.
import { BasePattern } from "@patternslib/patternslib/src/core/basepattern";
import registry from "@patternslib/patternslib/src/core/registry";
import events from "@patternslib/patternslib/src/core/events";

class Pattern extends BasePattern {
static name = "pat-base-url";
static trigger = "body";

init() {
events.add_event_listener(
window.navigation,
"navigate",
"thet-base-url--set",
this.set_base_url.bind(this)
);
}

set_base_url() {
let url = window.location.href;

// Split the following words from the URL as we want to get the
// contents absolute URL.
const split_words = [
// NOTE: order matters.
"/@@", // also catches @@folder_contents and @@edit
"/++", // traversal urls.
"/folder_contents",
"/edit",
"/view",
"#",
"?",
];

// Split all split words out of url
url = split_words.reduce((url_, split_) => url_.split(split_)[0], url);
// Remove the trailing slash
if (url[url.length -1] === "/") {
url = url.substring(0, url.length - 1)
}

// Set the contents absolute URL on `data-base-url`.
document.body.dataset.baseUrl = url;
}
}

registry.register(Pattern);
export default Pattern;
export { Pattern };
49 changes: 49 additions & 0 deletions src/pat/base-url/base-url.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import "@patternslib/patternslib/src/core/polyfills";
import utils from "@patternslib/patternslib/src/core/utils";
import Pattern from "./base-url";

describe("pat-base-url tests", () => {
afterEach(() => {
document.body.innerHTML = "";
});

it("is initialized correctly", async () => {
document.body.dataset.baseUrl = "http://localhost/not/okay"

new Pattern(document.body);
await utils.timeout(1); // wait a tick for async to settle.

expect(document.body.dataset.baseUrl).toBe("http://localhost/not/okay");
history.pushState(null, "", "/okay/okay");
expect(document.body.dataset.baseUrl).toBe("http://localhost/okay/okay");
});

it("Cleans the URL from views and traversal URLs.", async () => {
new Pattern(document.body);
await utils.timeout(1); // wait a tick for async to settle.

history.pushState(null, "", "/okay/@@okay");
expect(document.body.dataset.baseUrl).toBe("http://localhost/okay");

history.pushState(null, "", "/okay/++okay");
expect(document.body.dataset.baseUrl).toBe("http://localhost/okay");

history.pushState(null, "", "/okay/folder_contents");
expect(document.body.dataset.baseUrl).toBe("http://localhost/okay");

history.pushState(null, "", "/okay/edit");
expect(document.body.dataset.baseUrl).toBe("http://localhost/okay");

history.pushState(null, "", "/okay/view");
expect(document.body.dataset.baseUrl).toBe("http://localhost/okay");

history.pushState(null, "", "/okay/#okay");
expect(document.body.dataset.baseUrl).toBe("http://localhost/okay");

history.pushState(null, "", "/okay/?okay");
expect(document.body.dataset.baseUrl).toBe("http://localhost/okay");

history.pushState(null, "", "/okay/?okay#okay");
expect(document.body.dataset.baseUrl).toBe("http://localhost/okay");
});
});
157 changes: 112 additions & 45 deletions src/pat/navigationmarker/navigationmarker.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,119 @@
import $ from "jquery";
import Base from "@patternslib/patternslib/src/core/base";

export default Base.extend({
name: "navigationmarker",
trigger: ".pat-navigationmarker",
parser: "mockup",
init: function () {
const portal_url = document.body.dataset.portalUrl;
var href =
document.querySelector('head link[rel="canonical"]').href ||
import { BasePattern } from "@patternslib/patternslib/src/core/basepattern";
import events from "@patternslib/patternslib/src/core/events";
import Parser from "@patternslib/patternslib/src/core/parser";
import registry from "@patternslib/patternslib/src/core/registry";

export const parser = new Parser("navigationmarker");
parser.addArgument("portal-url", undefined);
parser.addArgument("parent-selector", undefined);

class Pattern extends BasePattern {
static name = "navigationmarker";
static trigger = ".pat-navigationmarker";
static parser = parser;

async init() {
this.portal_url = this.options.portalUrl || document.body.dataset.portalUrl;

events.add_event_listener(
window.navigation,
"navigate",
"pat-navigationmarker--history-changed",
(e) => {
this.scan_navigation(e);
},
);

this.scan_navigation();
}

scan_navigation(e) {
// TODO:‌Check, if all these ways to get the current URL are valid.
// TODO:Also check, if we could move that method to a core utility module, where it can be reused by other patterns, retrieving the current URL.
// TODO: Maybe even a central current-url store?
const href =
e?.destination?.url ||
document.body.dataset.baseUrl || // NOTE: TODO: Attention: If this is not yet updated, there would be an outdated URL.
document.querySelector('head link[rel="canonical"]')?.href ||
window.location.href;

$("a", this.$el).each(function () {
var navlink = this.href.replace("/view", "");
if (href.indexOf(navlink) !== -1) {
var parent = $(this).parent();
const anchors = this.el.querySelectorAll("a");

// check the input-openers within the path
var check = parent.find("> input");
if (check.length) {
check[0].checked = true;
}
for (const anchor of anchors) {
//const parent = anchor.parentElement;
const parent = this.options.parentSelector
? anchor.closest(this.options.parentSelector)
: anchor.parentElement;
const navlink = anchor.href.replace("/view", "");

// set "inPath" to all nav items which are within the current path
// check if parts of navlink are in canonical url parts
var hrefParts = href.split("/");
var navParts = navlink.split("/");
var inPath = false;
for (var i = 0, size = navParts.length; i < size; i++) {
// The last path-part must match.
inPath = false;
if (navParts[i] === hrefParts[i]) {
inPath = true;
}
}
if (navlink === portal_url && href !== portal_url) {
// Avoid marking "Home" with "inPath", when not actually there
inPath = false;
}
if (inPath) {
parent.addClass("inPath");
}
// We can exit early, if the navlink is not part of the current URL.
if (href.indexOf(navlink) === -1) {
this.clear(parent);
this.clear(anchor);
continue;
}

//// BBB
//// check the input-openers within the path
//const check = parent.querySelector(":scope > input");
//if (check) {
// check.checked = true;
//}

// set "current" to the current selected nav item, if it is in the navigation structure.
if (href === navlink) {
parent.addClass("current");
// set "inPath" to all nav items which are within the current path
// check if parts of navlink are in canonical url parts
//
const href_parts = href.split("/");
const nav_parts = navlink.split("/");
let inPath = false;

// The last part of the URL must match.
const nav_compare = nav_parts[nav_parts.length - 1];
const href_compare = href_parts[nav_parts.length - 1];
if (nav_compare === href_compare) {
inPath = true;
}

// Avoid marking "Home" with "inPath", when not actually there
if (navlink === this.portal_url && href !== this.portal_url) {
inPath = false;
}

// Set the class
if (inPath) {
// inPath is set along with current | TODO: OR NOT, verify.
parent.classList.add("inPath");
if (parent.tagName === "DETAILS") {
parent.open = true;
}
}
});
},
});

// set "current" to the current selected nav item, if it is in the navigation structure.
if (href === navlink) {
parent.classList.add("current");
anchor.classList.add("current");
}
}

this.el.dispatchEvent(events.generic_event(`${this.name}.scanned`));
}

clear(element) {
// Clear all classes
if (element.classList.contains("inPath")) {
element.classList.remove("inPath");
}
if (element.classList.contains("current")) {
element.classList.remove("current");
}
if (element.tagName === "DETAILS") {
element.open = false;
}
}
}

// Register Pattern class in the global pattern registry
registry.register(Pattern);

// Make it available
export default Pattern;
Loading
Loading