diff --git a/application/source/dom/util.mjs b/application/source/dom/util.mjs index ce375d943b8d80d2e48633ed32717a8333087f95..38c5e14175e3ef9eb74f7e0cff6521b4fd557339 100644 --- a/application/source/dom/util.mjs +++ b/application/source/dom/util.mjs @@ -8,7 +8,7 @@ import { getGlobal } from "../types/global.mjs"; import { validateString } from "../types/validate.mjs"; -export { getDocument, getWindow, getDocumentFragmentFromString }; +export { getDocument, getWindow, getDocumentFragmentFromString, findElementWithIdUpwards }; /** * This method fetches the document object @@ -151,3 +151,52 @@ function getDocumentFragmentFromString(html) { return template.content; } + + +/** + * Recursively searches upwards from a given element to find an ancestor element + * with a specified ID, considering both normal DOM and shadow DOM. + * + * @param {HTMLElement|ShadowRoot} element - The starting element or shadow root to search from. + * @param {string} targetId - The ID of the target element to find. + * @returns {HTMLElement|null} - The ancestor element with the specified ID, or null if not found. + * @memberOf Monster.DOM + * @since 3.29.0 + * @license AGPLv3 + * @copyright schukai GmbH + */ +function findElementWithIdUpwards(element, targetId) { + if (!element) { + return null; + } + + // Check if the current element has the target ID + if (element.id === targetId) { + return element; + } + + // Search within the current element's shadow root, if it exists + if (element.shadowRoot) { + const target = element.shadowRoot.getElementById(targetId); + if (target) { + return target; + } + } + + // If the current element is the document.documentElement, search within the main document + if (element === document.documentElement) { + const target = document.getElementById(targetId); + if (target) { + return target; + } + } + + // If the current element is inside a shadow root, search its host's ancestors + const rootNode = element.getRootNode(); + if (rootNode && rootNode instanceof ShadowRoot) { + return findElementWithIdUpwards(rootNode.host, targetId); + } + + // Otherwise, search the current element's parent + return findElementWithIdUpwards(element.parentElement, targetId); +} \ No newline at end of file diff --git a/development/test/cases/dom/find.mjs b/development/test/cases/dom/find.mjs new file mode 100644 index 0000000000000000000000000000000000000000..24bc0a2feaa80fb66aa04acbcd0ee4cd8c686b0c --- /dev/null +++ b/development/test/cases/dom/find.mjs @@ -0,0 +1,85 @@ +import { + findElementWithIdUpwards +} from "../../../../application/source/dom/util.mjs"; + +import { expect } from 'chai'; +import { JSDOM } from 'jsdom'; + +let originalEnvironment; + +function setupTestEnvironment() { + const { window } = new JSDOM('<!DOCTYPE html>', { pretendToBeVisual: true }); + + const { document, customElements, HTMLElement } = window; + originalEnvironment = { + document: globalThis.document, + customElements: globalThis.customElements, + HTMLElement: globalThis.HTMLElement, + ShadowRoot: globalThis.ShadowRoot, + }; + globalThis.document = document; + globalThis.customElements = customElements; + globalThis.HTMLElement = HTMLElement; + globalThis.ShadowRoot = window.ShadowRoot || class ShadowRoot {}; // Fallback for JSDOM + + class TestComponent extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + } + + if (!customElements.get('test-component')) { + customElements.define('test-component', TestComponent); + } +} + +function cleanupTestEnvironment() { + Object.assign(globalThis, originalEnvironment); +} + +describe('findElementWithIdUpwards', () => { + before(() => { + setupTestEnvironment(); + }); + + after(() => { + cleanupTestEnvironment(); + }); + + beforeEach(() => { + // Set up the DOM + document.body.innerHTML = ` + <div id="container"> + <div id="parent"> + <div id="child"></div> + </div> + </div> + `; + + const shadowHost = document.createElement('div'); + document.body.appendChild(shadowHost); + const shadowRoot = shadowHost.attachShadow({ mode: 'open' }); + const innerElement = document.createElement('div'); + innerElement.id = 'inner'; + shadowRoot.appendChild(innerElement); + }); + + it('should find the element with the target ID in the normal DOM', () => { + const child = document.getElementById('child'); + const result = findElementWithIdUpwards(child, 'parent'); + expect(result).to.equal(document.getElementById('parent')); + }); + + it('should find the element with the target ID in the shadow DOM', () => { + const innerElement = document.querySelector('div[shadowroot] > div'); + const result = findElementWithIdUpwards(innerElement, 'inner'); + expect(result).to.equal(innerElement); + }); + + it('should return null if the element with the target ID is not found', () => { + const child = document.getElementById('child'); + const result = findElementWithIdUpwards(child, 'nonexistent'); + expect(result).to.be.null; + }); +}); diff --git a/development/test/cases/dom/util.mjs b/development/test/cases/dom/util.mjs index 0ee3586470834805a2815e5737b32bfa7adb03e2..456c3452811c791879837696b4acefab44640b6c 100644 --- a/development/test/cases/dom/util.mjs +++ b/development/test/cases/dom/util.mjs @@ -1,5 +1,3 @@ -'use strict'; - import { getDocument, getWindow, getDocumentFragmentFromString } from "../../../../application/source/dom/util.mjs";