Skip to content
Snippets Groups Projects
Verified Commit c6bf7b09 authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

fix(reload): set shadowRoot to false

parent 4e9be9e5
No related branches found
No related tags found
No related merge requests found
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>reload in monster slider #293</title>
<script src="./293.mjs" type="module"></script>
</head>
<body>
<h1>reload in monster slider #293</h1>
<p></p>
<ul>
<li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/293">Issue #293</a></li>
<li><a href="/">Back to overview</a></li>
</ul>
<main>
<monster-notify data-monster-option-orientation="bottom right"></monster-notify>
<monster-monitor-attribute-errors
data-monster-option-features-notifyUser="true"
></monster-monitor-attribute-errors>
<monster-slider style="height:360px">
<div slot="next">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor"
class="bi bi-arrow-right-square-fill" viewBox="0 0 16 16">
<path d="M0 14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2zm4.5-6.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5a.5.5 0 0 1 0-1"/>
</svg>
</div>
<div slot="prev">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor"
class="bi bi-arrow-left-square-fill" viewBox="0 0 16 16">
<path d="M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2zm-4.5-6.5H5.707l2.147-2.146a.5.5 0 1 0-.708-.708l-3 3a.5.5 0 0 0 0 .708l3 3a.5.5 0 0 0 .708-.708L5.707 8.5H11.5a.5.5 0 0 0 0-1"/>
</svg>
</div>
<div class="slide" style=";background-color: var(--monster-bg-color-primary-2); color: var(--monster-color-primary-2)">
<monster-reload data-monster-option-url="/issue-293/slide1.html">
<div data-monster-role="container">
LOADER ...
</div>
</monster-reload>
</div>
<div class="slide" style=";background-color: var(--monster-bg-color-tertiary-2); color: var(--monster-color-tertiary-2)">
<div style="align-self: center;display: flex;width: 100%;justify-content: center;font-size: 3rem;">SLIDE 1a</div>
</div>
<div class="slide" style=";background-color: var(--monster-bg-color-secondary-3); color: var(--monster-color-secondary-3)">
<monster-reload data-monster-option-url="/issue-293/slide3.html">
<div data-monster-role="container">
LOADER ...
</div>
</monster-reload>
</div>
<div class="slide" style=";background-color: var(--monster-bg-color-primary-4); color: var(--monster-color-primary-4)">
<div style="align-self: center;display: flex;width: 100%;justify-content: center;font-size: 3rem;">SLIDE 3</div>
</div>
<div class="slide" style=";background-color: var(--monster-bg-color-tertiary-1); color: var(--monster-color-tertiary-1)">
<div style="align-self: center;display: flex;width: 100%;justify-content: center;font-size: 3rem;">SLIDE 4</div>
</div>
</main>
</body>
</html>
/**
* @file development/issues/open/293.mjs
* @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/293
* @description reload in monster slider
* @issue 293
*/
import "../../../source/components/style/property.pcss";
import "../../../source/components/style/link.pcss";
import "../../../source/components/style/color.pcss";
import "../../../source/components/style/theme.pcss";
import "../../../source/components/style/normalize.pcss";
import "../../../source/components/style/typography.pcss";
import "../../../source/components/layout/slider.mjs";
import "../../../source/components/form/reload.mjs";
import "../../../source/components/notify/notify.mjs";
import "../../../source/components/notify/monitor-attribute-errors.mjs";
const requestDelay = 1000
export default [
{
url: '/issue-293/slide1.html',
method: 'get',
rawResponse: async (req, res) => {
res.setHeader('Content-Type', 'text/html')
res.statusCode = 200
setTimeout(function() {
res.end('<div class="slider">Slide 1</div>')
}, requestDelay);
},
},
{
url: '/issue-293/slide2.html',
method: 'get',
rawResponse: async (req, res) => {
res.setHeader('Content-Type', 'text/html')
res.statusCode = 200
setTimeout(function() {
res.end('<div class="slider">Slide 2</div>')
}, requestDelay);
},
},
{
url: '/issue-293/slide3.html',
method: 'get',
rawResponse: async (req, res) => {
res.setHeader('Content-Type', 'text/html')
res.statusCode = 404
setTimeout(function() {
res.end('404 Not Found')
}, requestDelay);
},
},
];
\ No newline at end of file
......@@ -69,7 +69,7 @@ class Reload extends CustomElement {
* @property {Object} templates Template definitions
* @property {string} templates.main Main template
* @property {string} url url to fetch
* @property {string} reload onshow, always
* @property {string} reload onshow, always (onshow is default and means that the content is loaded when the element is visible, always means that the content is always loaded)
* @property {string} filter css selector
* @property {Object} fetch fetch options for the request
* @property {string} fetch.redirect error, follow, manual
......@@ -87,7 +87,7 @@ class Reload extends CustomElement {
templates: {
main: getTemplate.call(this),
},
shadowMode: null,
shadowMode: false,
url: null,
reload: "onshow",
filter: null,
......@@ -171,7 +171,13 @@ class Reload extends CustomElement {
this.setAttribute(ATTRIBUTE_FORM_URL, `${url}`);
}
return loadContent.call(this);
try {
return loadContent.call(this);
} catch (e) {
addErrorAttribute(this, e);
return Promise.reject(e)
}
}
}
......@@ -290,7 +296,7 @@ function loadContent() {
}
})
.catch((e) => {
throw e;
addErrorAttribute(this, e);
});
}
......
......@@ -16,124 +16,117 @@ import { isString } from "../../../types/is.mjs";
import { fireCustomEvent } from "../../../dom/events.mjs";
import { validateInstance, validateString } from "../../../types/validate.mjs";
/**
* Traverse the element's ancestors to find an existing Shadow DOM.
*
* @param {HTMLElement} element - The starting element.
* @returns {ShadowRoot|null} - The found Shadow DOM or null if none exists.
*/
function findShadowRoot(element) {
if (element instanceof ShadowRoot) return element;
if (!element.parentNode) return null;
return findShadowRoot(element.parentNode);
while (element) {
if (element?.shadowRoot) {
return element?.shadowRoot;
}
element = element?.parentNode;
}
return null;
}
/**
* @private
* @param {HTMLElement} element
* @param {string|URL} url
* @param {Object} options fetch options
* @param {Object} filter fetch options
* @return {Promise<Object>}
* @throws {Error} we won't be able to read the data
* @throws {Error} client error
* @throws {Error} undefined status or type
* @throws {TypeError} value is not an instance of
* @throws {TypeError} value is not a string
* Loads content from a URL and assigns it to an element.
* Optionally, the loaded content can be filtered using a CSS selector.
* Additionally, any <script> elements within the content are extracted and executed.
*
* @param {HTMLElement} element - The target element to insert the content.
* @param {string|URL} url - The URL from which to load the content.
* @param {Object} options - Options for the fetch call.
* @param {string} [filter] - Optional CSS selector to filter the loaded content.
* @returns {Promise<Object>} A promise that resolves to an object containing { content: string, type: string | null }.
* @throws {Error} When the content cannot be read or the response contains an error.
* @throws {TypeError} When the provided parameters do not match the expected types.
*/
function loadAndAssignContent(element, url, options, filter) {
return loadContent(url, options).then((response) => {
let content = response.content;
// Optional filtering: if a valid, non-empty CSS selector is provided,
// only the matching elements will be retained.
if (isString(filter) && filter !== "") {
const t = document.createElement("div");
const c = document.createElement("div");
c.innerHTML = content;
for (const [, node] of c.querySelectorAll(filter).entries()) {
t.appendChild(node);
}
content = t.innerHTML;
const filteredContainer = document.createElement("div");
const tempContainer = document.createElement("div");
tempContainer.innerHTML = content;
const matchingNodes = tempContainer.querySelectorAll(filter);
matchingNodes.forEach((node) => {
filteredContainer.appendChild(node);
});
content = filteredContainer.innerHTML;
}
const t = document.createElement("div");
t.innerHTML = content;
const scripts = t.querySelectorAll("script");
for (const [, script] of scripts.entries()) {
const s = document.createElement("script");
s.innerHTML = script.innerHTML;
if (script.src) s.src = script.src;
if (script.type) s.type = script.type;
if (script.async) s.async = script.async;
if (script.defer) s.defer = script.defer;
if (script.crossOrigin) s.crossOrigin = script.crossOrigin;
if (script.integrity) s.integrity = script.integrity;
if (script.referrerPolicy) s.referrerPolicy = script.referrerPolicy;
document.head.appendChild(s);
t.removeChild(script);
}
// Temporary container for processing the content and extracting scripts
const tempDiv = document.createElement("div");
tempDiv.innerHTML = content;
// Extract and execute all <script> elements by appending them to the document head
const scriptElements = tempDiv.querySelectorAll("script");
scriptElements.forEach((oldScript) => {
const newScript = document.createElement("script");
if (oldScript.src) newScript.src = oldScript.src;
if (oldScript.type) newScript.type = oldScript.type;
if (oldScript.async) newScript.async = oldScript.async;
if (oldScript.defer) newScript.defer = oldScript.defer;
if (oldScript.crossOrigin) newScript.crossOrigin = oldScript.crossOrigin;
if (oldScript.integrity) newScript.integrity = oldScript.integrity;
if (oldScript.referrerPolicy) newScript.referrerPolicy = oldScript.referrerPolicy;
newScript.textContent = oldScript.textContent;
document.head.appendChild(newScript);
if (oldScript.parentNode) {
oldScript.parentNode.removeChild(oldScript);
}
});
validateInstance(element, HTMLElement).innerHTML = t.innerHTML;
// Assign the processed content to the target element
validateInstance(element, HTMLElement).innerHTML = tempDiv.innerHTML;
const root = findShadowRoot(element);
if (root !== null) {
element = root.host;
// If the element is within a Shadow DOM, use the host as the event target
const shadowRoot = findShadowRoot(element);
const eventTarget = shadowRoot !== null ? shadowRoot.host : element;
if (eventTarget instanceof HTMLElement) {
fireCustomEvent(eventTarget, "monster-fetched", { url });
}
fireCustomEvent(element, "monster-fetched", {
url,
});
return response;
});
}
/**
* @private
* @param {string|URL} url
* @param {Object} options fetch options
* @return {Promise<string>}
* @throws {Error} we won't be able to read the data
* @throws {Error} client error
* @throws {Error} undefined status or type
* @throws {TypeError} value is not a string
* Loads content from a URL using fetch and returns an object with the loaded content
* and the Content-Type header.
*
* @param {string|URL} url - The URL from which to load the content.
* @param {Object} options - Options for the fetch call.
* @returns {Promise<Object>} A promise that resolves to an object { content: string, type: string | null }.
* @throws {Error} When the content cannot be read or the response contains an error.
* @throws {TypeError} When the URL cannot be validated as a string.
*/
function loadContent(url, options) {
if (url instanceof URL) {
url = url.toString();
}
return fetch(validateString(url), options).then((response) => {
// The ok read-only property of the Response interface contains a
// Boolean stating whether the response was successful (status in the range 200-299) or not.
if (response?.ok !== true) {
// @see https://developer.mozilla.org/en-US/docs/Web/API/Response/type
if (
["error", "opaque", "opaqueredirect"].indexOf(response?.type) !== -1
) {
throw new Error(
`we won't be able to read the data (${response?.type})`,
);
if (!response.ok) {
if (["error", "opaque", "opaqueredirect"].includes(response.type)) {
throw new Error(`we won't be able to read the data (${response.type})`);
}
const statusClass = `${response?.status}`.substring(0, 1);
switch (statusClass) {
case "4":
throw new Error(`client error ${response?.statusText}`);
break;
default:
throw new Error(
`undefined status (${response?.status} / ${response?.statusText}) or type (${response?.type})`,
);
const statusClass = String(response.status).charAt(0);
if (statusClass === "4") {
throw new Error(`client error ${response.statusText}`);
}
throw new Error(`undefined status (${response.status} / ${response.statusText}) or type (${response.type})`);
}
return new Promise(function (resolve, reject) {
response
.text()
.then((content) => {
resolve({
content,
type: response.headers.get("Content-Type"),
});
})
.catch(reject);
});
return response.text().then((content) => ({
content,
type: response.headers.get("Content-Type"),
}));
});
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment