import { MediaStateReceiverAttributes } from '../constants.js'; import type MediaController from '../media-controller.js'; export function namedNodeMapToObject(namedNodeMap: NamedNodeMap) { const obj = {}; for (const attr of namedNodeMap) { obj[attr.name] = attr.value; } return obj; } /** * Get the media controller element from the `mediacontroller` attribute or closest ancestor. * @param host - The element to search for the media controller. */ export function getMediaController( host: HTMLElement ): MediaController | undefined { return ( getAttributeMediaController(host) ?? closestComposedNode(host, 'media-controller') ); } /** * Get the media controller element from the `mediacontroller` attribute. * @param host - The element to search for the media controller. * @return */ export function getAttributeMediaController( host: HTMLElement ): MediaController | undefined { const { MEDIA_CONTROLLER } = MediaStateReceiverAttributes; const mediaControllerId = host.getAttribute(MEDIA_CONTROLLER); if (mediaControllerId) { return getDocumentOrShadowRoot(host)?.getElementById( mediaControllerId ) as MediaController; } } export const updateIconText = ( svg: HTMLElement, value: string, selector: string = '.value' ): void => { const node = svg.querySelector(selector); if (!node) return; node.textContent = value; }; export const getAllSlotted = ( el: HTMLElement, name: string ): HTMLCollection | HTMLElement[] => { const slotSelector = `slot[name="${name}"]`; const slot: HTMLSlotElement = el.shadowRoot.querySelector(slotSelector); if (!slot) return []; return slot.children; }; export const getSlotted = (el: HTMLElement, name: string): HTMLElement => getAllSlotted(el, name)[0] as HTMLElement; /** * * @param {{ contains?: Node['contains'] }} [rootNode] * @param {Node} [childNode] * @returns boolean */ export const containsComposedNode = ( rootNode: Node, childNode: Node ): boolean => { if (!rootNode || !childNode) return false; if (rootNode?.contains(childNode)) return true; return containsComposedNode( rootNode, (childNode.getRootNode() as ShadowRoot).host ); }; export const closestComposedNode = ( childNode: Element, selector: string ): T => { if (!childNode) return null; const closest = childNode.closest(selector); if (closest) return closest as T; return closestComposedNode( (childNode.getRootNode() as ShadowRoot).host, selector ); }; /** * Get the active element, accounting for Shadow DOM subtrees. * @param root - The root node to search for the active element. */ export function getActiveElement( root: Document | ShadowRoot = document ): HTMLElement { const activeEl = root?.activeElement; if (!activeEl) return null; return getActiveElement(activeEl.shadowRoot) ?? (activeEl as HTMLElement); } /** * Gets the document or shadow root of a node, not the node itself which can lead to bugs. * https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode#return_value * @param node - The node to get the root node from. */ export function getDocumentOrShadowRoot( node: Node ): Document | ShadowRoot | null { const rootNode = node?.getRootNode?.(); if (rootNode instanceof ShadowRoot || rootNode instanceof Document) { return rootNode; } return null; } /** * Checks if the element is visible includes opacity: 0 and visibility: hidden. * @param element - The element to check for visibility. */ export function isElementVisible( element: HTMLElement, { depth = 3, checkOpacity = true, checkVisibilityCSS = true } = {} ): boolean { // Supported by Chrome and Firefox https://caniuse.com/mdn-api_element_checkvisibility // https://drafts.csswg.org/cssom-view-1/#dom-element-checkvisibility // @ts-ignore if (element.checkVisibility) { // @ts-ignore return element.checkVisibility({ checkOpacity, checkVisibilityCSS, }); } // Check if the element or its ancestors are hidden. let el = element; while (el && depth > 0) { const style = getComputedStyle(el); if ( (checkOpacity && style.opacity === '0') || (checkVisibilityCSS && style.visibility === 'hidden') || style.display === 'none' ) { return false; } el = el.parentElement; depth--; } return true; } export type Point = { x: number; y: number }; /** * Get progress ratio of a point on a line segment. * @param x - The x coordinate of the point. * @param y - The y coordinate of the point. * @param p1 - The first point of the line segment. * @param p2 - The second point of the line segment. */ export function getPointProgressOnLine( x: number, y: number, p1: Point, p2: Point ): number { const dx = p2.x - p1.x; const dy = p2.y - p1.y; const lengthSquared = dx * dx + dy * dy; if (lengthSquared === 0) return 0; // Avoid division by zero if p1 === p2 const projection = ((x - p1.x) * dx + (y - p1.y) * dy) / lengthSquared; return Math.max(0, Math.min(1, projection)); // Clamp between 0 and 1 } export function distance(p1: Point, p2: Point) { return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); } /** * Get or insert a CSSStyleRule with a selector in an element containing