Skip to content
Draft
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
56 changes: 56 additions & 0 deletions src/pat/base-url/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# pat-base-url

Update `body[data-base-url]` and `body[data-view-url]` on navigation changes.

## Description

Plone maintains the `data-base-url` and `data-view-url` data attributes on the
body tag. The base-url is the URL of the current context, the view-url is the
full URL including the view name and both can be used in JavaScript.

This needs to be updated, when navigating through the site with AJAX, for
example using Patternslib `pat-inject` with the `history:record` setting, which
updates the URL bar after a ajax call:

```html
<a
class="pat-inject"
href="news"
data-pat-inject="
source: body;
target: body;
history: record;
"
>
Click here.
</a>
```

The logic is as follows:

- Get active on a `pat-inject-before-history-update` event from pat-inject in
Patternslib ([See this PR](https://github.com/Patternslib/Patterns/pull/1280)).
The AJAX response data is stored on the event.
Note:‌ This pattern can listen to other events, having different event payload
data structures as well as other areas throwing this event.

- Add or update either or both `data-base-url` and `data-view-url` if it exists
in the ajax response from pat-inject.

- Remove it from the body tag otherwise, so that we don't provide wrong URLs
when the URL bar changes. A fallback should be provided, see below.

## Considerations

- When `data-base-url` and `data-view-url` are not available, a fallback should
be used.

- The fallback can be `<link rel="canonical" href="">` or `window.location`.

- A utility method to get the `base-url` and `view-url` would be good, which
uses a fallback, if they are not provided on the body as data attributes.

- This pattern is sorted early and executes even before any other pattern, when
running sequentially. But this order doesn't matter, since this Pattern is only
getting active on an event and the execution order is not defined for events
(whatever comes first executes first) and actually shouldn't matter anyway.
60 changes: 60 additions & 0 deletions src/pat/base-url/base-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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";

// Sort this pattern very early.
// It needs to get active before any other patterns, as some depend on a
// `data-base-url` value on the body tag.
static order = 10;

init() {
events.add_event_listener(
document,
"pat-inject-before-history-update",
"base-url--set",
(ev) => this.set_base_url(ev),
);
}

extract_data_attributes(html_string) {
if (!html_string) {
return { base_url: null, view_url: null };
}
const parser = new DOMParser();
const doc = parser.parseFromString(html_string, "text/html");
const body = doc.body;

return {
base_url: body?.getAttribute("data-base-url") || null,
view_url: body?.getAttribute("data-view-url") || null,
};
}

set_base_url(ev) {
const html_string = ev?.detail?.jqxhr?.responseText;
const data_attributes = this.extract_data_attributes(html_string);

// Set data-base-url - or remove it, if not set.
if (data_attributes.base_url !== null) {
document.body.dataset.baseUrl = data_attributes.base_url;
} else {
delete document.body.dataset.baseUrl;
}

// Set data-view-url - or remove it, if not set.
if (data_attributes.view_url !== null) {
document.body.dataset.viewUrl = data_attributes.view_url;
} else {
delete document.body.dataset.viewUrl;
}
}
}

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

describe("pat-base-url tests", () => {
let pattern_instance;

beforeEach(() => {
// Clean up any existing attributes
delete document.body.dataset.baseUrl;
delete document.body.dataset.viewUrl;
});

afterEach(() => {
document.body.innerHTML = "";
delete document.body.dataset.baseUrl;
delete document.body.dataset.viewUrl;
if (pattern_instance) {
pattern_instance = null;
}
});

it("is initialized correctly", async () => {
pattern_instance = new Pattern(document.body);
await utils.timeout(1); // wait a tick for async to settle

// Check that the pattern instance exists
expect(pattern_instance).toBeDefined();
expect(pattern_instance.el).toBe(document.body);
});

it("extracts base-url and view-url from HTML string", () => {
pattern_instance = new Pattern(document.body);

const html_string = `
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body data-base-url="http://example.com/content" data-view-url="http://example.com/content/view">
<div>Content</div>
</body>
</html>
`;

const result = pattern_instance.extract_data_attributes(html_string);

expect(result.base_url).toBe("http://example.com/content");
expect(result.view_url).toBe("http://example.com/content/view");
});

it("handles HTML string without data attributes", () => {
pattern_instance = new Pattern(document.body);

const html_string = `
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body>
<div>Content</div>
</body>
</html>
`;

const result = pattern_instance.extract_data_attributes(html_string);

expect(result.base_url).toBeNull();
expect(result.view_url).toBeNull();
});

it("handles empty or invalid HTML string", () => {
pattern_instance = new Pattern(document.body);

expect(pattern_instance.extract_data_attributes("")).toEqual({
base_url: null,
view_url: null,
});

expect(pattern_instance.extract_data_attributes(null)).toEqual({
base_url: null,
view_url: null,
});

expect(pattern_instance.extract_data_attributes(undefined)).toEqual({
base_url: null,
view_url: null,
});
});

it("sets base-url and view-url from pat-inject-before-history-update event", async () => {
pattern_instance = new Pattern(document.body);
await utils.timeout(1); // wait for initialization

const response_html = `
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body data-base-url="http://example.com/new-base" data-view-url="http://example.com/new-view">
<div>New content</div>
</body>
</html>
`;

// Mock jqxhr object
const mock_jqxhr = {
responseText: response_html,
};

// Dispatch the event that the pattern listens to
const custom_event = new CustomEvent("pat-inject-before-history-update", {
detail: {
jqxhr: mock_jqxhr,
},
});

document.dispatchEvent(custom_event);

expect(document.body.dataset.baseUrl).toBe("http://example.com/new-base");
expect(document.body.dataset.viewUrl).toBe("http://example.com/new-view");
});

it("removes attributes when no data attributes are found in response", async () => {
// Set initial values
document.body.dataset.baseUrl = "http://example.com/old-base";
document.body.dataset.viewUrl = "http://example.com/old-view";

pattern_instance = new Pattern(document.body);
await utils.timeout(1); // wait for initialization

const response_html = `
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body>
<div>Content without data attributes</div>
</body>
</html>
`;

const mock_jqxhr = {
responseText: response_html,
};

const custom_event = new CustomEvent("pat-inject-before-history-update", {
detail: {
jqxhr: mock_jqxhr,
},
});

document.dispatchEvent(custom_event);

// Attributes should be removed when null values are set
expect(document.body.hasAttribute("data-base-url")).toBe(false);
expect(document.body.hasAttribute("data-view-url")).toBe(false);
});

it("handles event without jqxhr or responseText", async () => {
// Set initial values
document.body.dataset.baseUrl = "http://example.com/initial";
document.body.dataset.viewUrl = "http://example.com/initial-view";

pattern_instance = new Pattern(document.body);
await utils.timeout(1); // wait for initialization

// Event without detail
const custom_event1 = new CustomEvent("pat-inject-before-history-update");
document.dispatchEvent(custom_event1);

// Attributes should be removed when null values are processed
expect(document.body.hasAttribute("data-base-url")).toBe(false);
expect(document.body.hasAttribute("data-view-url")).toBe(false);

// Reset for next test
document.body.dataset.baseUrl = "http://example.com/reset";
document.body.dataset.viewUrl = "http://example.com/reset-view";

// Event with detail but no jqxhr
const custom_event2 = new CustomEvent("pat-inject-before-history-update", {
detail: {},
});
document.dispatchEvent(custom_event2);

// Attributes should be removed again
expect(document.body.hasAttribute("data-base-url")).toBe(false);
expect(document.body.hasAttribute("data-view-url")).toBe(false);
});

it("handles malformed HTML gracefully", async () => {
pattern_instance = new Pattern(document.body);
await utils.timeout(1); // wait for initialization

const malformed_html = "<div>Incomplete HTML without body tag";

const mock_jqxhr = {
responseText: malformed_html,
};

const custom_event = new CustomEvent("pat-inject-before-history-update", {
detail: {
jqxhr: mock_jqxhr,
},
});

// Should not throw an error
expect(() => {
document.dispatchEvent(custom_event);
}).not.toThrow();

// Should remove attributes since no valid body with data attributes was found
expect(document.body.hasAttribute("data-base-url")).toBe(false);
expect(document.body.hasAttribute("data-view-url")).toBe(false);
});

it("only extracts from body tag, not other elements", () => {
pattern_instance = new Pattern(document.body);

const html_string = `
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body>
<div data-base-url="http://wrong.com/div" data-view-url="http://wrong.com/div-view">
Div with data attributes (should be ignored)
</div>
</body>
</html>
`;

const result = pattern_instance.extract_data_attributes(html_string);

expect(result.base_url).toBeNull();
expect(result.view_url).toBeNull();
});
});
1 change: 1 addition & 0 deletions src/patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import "@patternslib/patternslib/src/pat/depends/depends";
// Import all used patterns for the bundle to be generated
import "./pat/autotoc/autotoc";
import "./pat/backdrop/backdrop";
import "./pat/base-url/base-url";
import "./pat/contentloader/contentloader";
import "./pat/contentbrowser/contentbrowser";
import "./pat/cookietrigger/cookietrigger";
Expand Down
Loading