UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

404 lines (367 loc) 10.7 kB
/** * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact Volker Schukai. * * SPDX-License-Identifier: AGPL-3.0 */ import { getGlobal } from "../types/global.mjs"; import { validateString } from "../types/validate.mjs"; export { getDocument, getWindow, getDocumentFragmentFromString, findElementWithIdUpwards, getContainingDocument, getRegisteredCustomElements, findElementWithSelectorUpwards, waitForCustomElement, }; /** * 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])); * } * ``` * * @return {object} * @license AGPLv3 * @since 1.6.0 * @copyright Volker Schukai * @throws {Error} not supported environment */ function getDocument() { const 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])); * } * ``` * * @return {object} * @license AGPLv3 * @since 1.6.0 * @copyright Volker Schukai * @throws {Error} not supported environment */ function getWindow() { const 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])); * } * ``` * * @return {DocumentFragment} * @license AGPLv3 * @since 1.6.0 * @copyright Volker Schukai * @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. * @return {HTMLElement|null} - The ancestor element with the specified ID, or null if not found. * @since 3.29.0 * @license AGPLv3 * @copyright Volker Schukai */ 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 || element === document) { 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); } /** * Recursively searches upwards from a given element to find an ancestor element * with a specified selector, considering both normal DOM and shadow DOM. * This method is useful for finding a parent element with a specific class. * * @param {HTMLElement|ShadowRoot} element - The starting element or shadow root to search from. * @param {string} selector - The selector of the target element to find. * @return {HTMLElement|null} - The ancestor element with the specified selector, or null if not found. * @since 3.55.0 */ function findElementWithSelectorUpwards(element, selector) { if (!element || !selector) { return null; } // Search within the current element's shadow root, if it exists if (element.shadowRoot) { const target = element.shadowRoot.querySelector(selector); if (target) { return target; } } if (element === document.documentElement) { const target = document.querySelector(selector); 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 findElementWithSelectorUpwards(rootNode.host, selector); } // Otherwise, search the current element's parent return findElementWithSelectorUpwards(element.parentElement, selector); } /** * @private * @param {HTMLElement} element * @return {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 * @return {*} * @throws {Error} Invalid argument. Expected an HTMLElement. * @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; } /** * Returns a list of all registered custom elements in the current document. * * @return {string[]} * @since 4.0.0 * @return {string[]} */ function getRegisteredCustomElements() { const customElementTags = Array.from(document.querySelectorAll("*")) .map((tag) => tag.tagName.toLowerCase()) .filter((tagName) => tagName.includes("-") && customElements.get(tagName)); return Array.from(new Set(customElementTags)); } /** * Waits until a specific custom element instance is defined and upgraded. * * @param {HTMLElement} element * @param {Object} [options] * @param {string} [options.method] - Optional method to wait for on the instance. * @param {string} [options.tagName] - Optional tag name override. * @param {number|null} [options.timeout=2000] - Timeout in ms; set null to disable. * @return {Promise<HTMLElement>} * @since 4.1.0 */ function waitForCustomElement( element, { method = null, tagName = null, timeout = 2000, readyCheck = null } = {}, ) { if (!(element instanceof HTMLElement)) { return Promise.reject( new Error("Invalid argument. Expected an HTMLElement."), ); } const name = (tagName || element.tagName || "").toLowerCase(); if (!name.includes("-")) { return Promise.reject( new Error("Invalid argument. Expected a custom element tag name."), ); } const window = getWindow(); const registry = window.customElements; if (!registry) { return Promise.reject(new Error("customElements is not supported.")); } return registry.whenDefined(name).then( () => new Promise((resolve, reject) => { if (typeof registry.upgrade === "function") { registry.upgrade(element); } const start = Date.now(); const isReady = () => { if (method && typeof element[method] !== "function") { return false; } if (typeof readyCheck === "function") { return readyCheck(element) === true; } return true; }; const check = () => { if (isReady()) { resolve(element); return; } if ( typeof timeout === "number" && timeout >= 0 && Date.now() - start > timeout ) { reject( new Error(`Timed out waiting for "${method}" on <${name}>.`), ); return; } window.setTimeout(check, 10); }; window.setTimeout(check, 10); }), ); }