diff --git a/development/issues/closed/293.html b/development/issues/closed/293.html new file mode 100644 index 0000000000000000000000000000000000000000..59afbea572cc86317d0581ee312a37cf06df1347 --- /dev/null +++ b/development/issues/closed/293.html @@ -0,0 +1,62 @@ +<!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> diff --git a/development/issues/closed/293.mjs b/development/issues/closed/293.mjs new file mode 100644 index 0000000000000000000000000000000000000000..73b3d83109723e9e54505d9c219003b0faa22ec7 --- /dev/null +++ b/development/issues/closed/293.mjs @@ -0,0 +1,18 @@ +/** +* @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"; + diff --git a/development/mock/issue-293.js b/development/mock/issue-293.js new file mode 100644 index 0000000000000000000000000000000000000000..4b510b8bee676f221d3c98e329469a50703f5b39 --- /dev/null +++ b/development/mock/issue-293.js @@ -0,0 +1,46 @@ + + +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 diff --git a/source/components/form/reload.mjs b/source/components/form/reload.mjs index 214a3f740fe7bdcf7b7c301c2212a9c38ce01c5a..41a91832f8b26b2e7d3dfa6ddfd772fa67344271 100644 --- a/source/components/form/reload.mjs +++ b/source/components/form/reload.mjs @@ -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); }); } diff --git a/source/components/form/util/fetch.mjs b/source/components/form/util/fetch.mjs index 836a05069b665c7041a03f5dc39c2a2133ec8643..6c97acf89a94562e56c3057e9ee55c81597e7a16 100644 --- a/source/components/form/util/fetch.mjs +++ b/source/components/form/util/fetch.mjs @@ -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"), + })); }); }