import { hasClass } from './class' let idCounter = 0 export function uniqueId() { idCounter += 1 return `v${idCounter}` } export function ensureId(elem: Element) { if (elem.id == null || elem.id === '') { elem.id = uniqueId() } return elem.id } /** * Returns true if object is an instance of SVGGraphicsElement. * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGGraphicsElement */ export function isSVGGraphicsElement( elem?: any | null, ): elem is SVGGraphicsElement { if (elem == null) { return false } return typeof elem.getScreenCTM === 'function' && elem instanceof SVGElement } export const ns = { svg: 'http://www.w3.org/2000/svg', xmlns: 'http://www.w3.org/2000/xmlns/', xml: 'http://www.w3.org/XML/1998/namespace', xlink: 'http://www.w3.org/1999/xlink', xhtml: 'http://www.w3.org/1999/xhtml', } export const svgVersion = '1.1' export function createElement( tagName: string, doc: Document = document, ): T { return doc.createElement(tagName) as any as T } export function createElementNS( tagName: string, namespaceURI: string = ns.xhtml, doc: Document = document, ): T { return doc.createElementNS(namespaceURI, tagName) as any as T } export function createSvgElement( tagName: string, doc: Document = document, ): T { return createElementNS(tagName, ns.svg, doc) as T } export function createSvgDocument(content?: string) { if (content) { const xml = `${content}` // lgtm[js/html-constructed-from-input] const { documentElement } = parseXML(xml, { async: false }) return documentElement as any as SVGSVGElement } const svg = document.createElementNS(ns.svg, 'svg') svg.setAttributeNS(ns.xmlns, 'xmlns:xlink', ns.xlink) svg.setAttribute('version', svgVersion) return svg as SVGSVGElement } export function parseXML( data: string, options: { async?: boolean mimeType?: | 'text/html' | 'text/xml' | 'application/xml' | 'application/xhtml+xml' | 'image/svg+xml' } = {}, ) { let xml try { const parser = new DOMParser() if (options.async != null) { const instance = parser as any instance.async = options.async } xml = parser.parseFromString(data, options.mimeType || 'text/xml') } catch (error) { xml = undefined } if (!xml || xml.getElementsByTagName('parsererror').length) { throw new Error(`Invalid XML: ${data}`) } return xml } export function tagName(node: Element, lowercase = true) { const nodeName = node.nodeName return lowercase ? nodeName.toLowerCase() : nodeName.toUpperCase() } export function index(elem: Element) { let index = 0 let node = elem.previousSibling while (node) { if (node.nodeType === 1) { index += 1 } node = node.previousSibling } return index } export function find(elem: Element, selector: string) { return elem.querySelectorAll(selector) } export function findOne(elem: Element, selector: string) { return elem.querySelector(selector) } export function findParentByClass( elem: Element, className: string, terminator?: Element, ) { const ownerSVGElement = (elem as SVGElement).ownerSVGElement let node = elem.parentNode while (node && node !== terminator && node !== ownerSVGElement) { if (hasClass(node as Element, className)) { return node } node = node.parentNode } return null } export function contains(parent: Element, child: Element) { const bup = child && child.parentNode return ( parent === bup || !!(bup && bup.nodeType === 1 && parent.compareDocumentPosition(bup) & 16) // eslint-disable-line no-bitwise ) } export function remove(elem: Element | Element[] | null) { if (elem) { const elems = Array.isArray(elem) ? elem : [elem] elems.forEach((item) => { if (item.parentNode) { item.parentNode.removeChild(item) } }) } } export function empty(elem: Element) { while (elem.firstChild) { elem.removeChild(elem.firstChild) } } export function append( elem: Element, elems: Element | DocumentFragment | (Element | DocumentFragment)[], ) { const arr = Array.isArray(elems) ? elems : [elems] arr.forEach((child) => { if (child != null) { elem.appendChild(child) } }) } export function prepend( elem: Element, elems: Element | DocumentFragment | (Element | DocumentFragment)[], ) { const child = elem.firstChild return child ? before(child as HTMLElement, elems) : append(elem, elems) } export function before( elem: Element, elems: Element | DocumentFragment | (Element | DocumentFragment)[], ) { const parent = elem.parentNode if (parent) { const arr = Array.isArray(elems) ? elems : [elems] arr.forEach((child) => { if (child != null) { parent.insertBefore(child, elem) } }) } } export function after( elem: Element, elems: Element | DocumentFragment | (Element | DocumentFragment)[], ) { const parent = elem.parentNode if (parent) { const arr = Array.isArray(elems) ? elems : [elems] arr.forEach((child) => { if (child != null) { parent.insertBefore(child, elem.nextSibling) } }) } } export function appendTo(elem: Element, target: Element) { if (target != null) { target.appendChild(elem) } } export function isElement(x: any): x is HTMLElement { return !!x && x.nodeType === 1 } // Determines whether a node is an HTML node export function isHTMLElement(elem: any): elem is HTMLElement { try { // Using W3 DOM2 (works for FF, Opera and Chrome) return elem instanceof HTMLElement } catch (e) { // Browsers not supporting W3 DOM2 don't have HTMLElement and // an exception is thrown and we end up here. Testing some // properties that all elements have (works on IE7) return ( typeof elem === 'object' && elem.nodeType === 1 && typeof elem.style === 'object' && typeof elem.ownerDocument === 'object' ) } } export function children(parent: Element, className?: string) { const matched: Element[] = [] let elem = parent.firstChild for (; elem; elem = elem.nextSibling) { if (elem.nodeType === 1) { if (!className || hasClass(elem as Element, className)) { matched.push(elem as Element) } } } return matched }