/** * Copyright schukai GmbH and contributors 2023. All Rights Reserved. * Node module: @schukai/monster * This file is licensed under the AGPLv3 License. * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html */ import { getGlobal } from "../types/global.mjs"; import { validateString } from "../types/validate.mjs"; export { getDocument, getWindow, getDocumentFragmentFromString, findElementWithIdUpwards, getContainingDocument }; /** * This method fetches the document object * * In nodejs this functionality can be performed with [jsdom](https://www.npmjs.com/package/jsdom). * * ``` * import {JSDOM} from "jsdom" * if (typeof window !== "object") { * const {window} = new JSDOM('', { * url: 'http://example.com/', * pretendToBeVisual: true * }); * * [ * 'self', * 'document', * 'Document', * 'Node', * 'Element', * 'HTMLElement', * 'DocumentFragment', * 'DOMParser', * 'XMLSerializer', * 'NodeFilter', * 'InputEvent', * 'CustomEvent' * ].forEach(key => (getGlobal()[key] = window[key])); * } * ``` * * @returns {object} * @license AGPLv3 * @since 1.6.0 * @copyright schukai GmbH * @memberOf Monster.DOM * @throws {Error} not supported environment */ function getDocument() { let document = getGlobal()?.["document"]; if (typeof document !== "object") { throw new Error("not supported environment"); } return document; } /** * This method fetches the window object * * In nodejs this functionality can be performed with [jsdom](https://www.npmjs.com/package/jsdom). * * ``` * import {JSDOM} from "jsdom" * if (typeof window !== "object") { * const {window} = new JSDOM('', { * url: 'http://example.com/', * pretendToBeVisual: true * }); * * getGlobal()['window']=window; * * [ * 'self', * 'document', * 'Document', * 'Node', * 'Element', * 'HTMLElement', * 'DocumentFragment', * 'DOMParser', * 'XMLSerializer', * 'NodeFilter', * 'InputEvent', * 'CustomEvent' * ].forEach(key => (getGlobal()[key] = window[key])); * } * ``` * * @returns {object} * @license AGPLv3 * @since 1.6.0 * @copyright schukai GmbH * @memberOf Monster.DOM * @throws {Error} not supported environment */ function getWindow() { let window = getGlobal()?.["window"]; if (typeof window !== "object") { throw new Error("not supported environment"); } return window; } /** * This method fetches the document object * * In nodejs this functionality can be performed with [jsdom](https://www.npmjs.com/package/jsdom). * * ``` * import {JSDOM} from "jsdom" * if (typeof window !== "object") { * const {window} = new JSDOM('', { * url: 'http://example.com/', * pretendToBeVisual: true * }); * * [ * 'self', * 'document', * 'Document', * 'Node', * 'Element', * 'HTMLElement', * 'DocumentFragment', * 'DOMParser', * 'XMLSerializer', * 'NodeFilter', * 'InputEvent', * 'CustomEvent' * ].forEach(key => (getGlobal()[key] = window[key])); * } * ``` * * @returns {DocumentFragment} * @license AGPLv3 * @since 1.6.0 * @copyright schukai GmbH * @memberOf Monster.DOM * @throws {Error} not supported environment * @throws {TypeError} value is not a string */ function getDocumentFragmentFromString(html) { validateString(html); const document = getDocument(); const template = document.createElement("template"); template.innerHTML = 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); } /** * @private * @param {HTMLElement} element * @returns {HTMLElement|null} */ function traverseShadowRoots(element) { let currentRoot = element.shadowRoot; let currentParent = element.parentNode; while ( currentParent && currentParent.nodeType !== Node.DOCUMENT_NODE && currentParent.nodeType !== Node.DOCUMENT_FRAGMENT_NODE ) { if (currentRoot && currentRoot.parentNode) { currentParent = currentRoot.parentNode; currentRoot = currentParent.shadowRoot; } else if (currentParent.parentNode) { currentParent = currentParent.parentNode; currentRoot = null; } else if (currentRoot && currentRoot.host && currentRoot.host.nodeType === Node.DOCUMENT_NODE) { currentParent = currentRoot.host; currentRoot = null; } else { currentParent = null; currentRoot = null; } } return currentParent; } /** * Recursively searches upwards from a given element to find an ancestor element * * @param {HTMLElement} element * @returns {*} * @throws {Error} Invalid argument. Expected an HTMLElement. * @memberOf Monster.DOM * @since 3.36.0 */ function getContainingDocument(element) { if ( !element || !(element instanceof HTMLElement || element instanceof element.ownerDocument.defaultView.HTMLElement) ) { throw new Error("Invalid argument. Expected an HTMLElement."); } return traverseShadowRoots(element) || null; }