(elm?: T | null): T | undefined | null {
while (elm?.firstChild) {
if (elm.lastChild) {
elm.removeChild(elm.lastChild);
}
}
return elm;
}
/** Get HTML element offset with pure JS */
export function getOffset(element?: HTMLElement): HtmlElementPosition | undefined {
if (element) {
const { top, left, bottom, right } = element.getBoundingClientRect();
return {
top: top + window.pageYOffset,
left: left + window.pageXOffset,
bottom,
right,
};
}
return undefined;
}
export function getSize(elm: HTMLElement | undefined, mode: 'inner' | 'outer' | 'scroll', type: 'height' | 'width') {
if (!elm) {
return 0;
}
// first try defined style width or offsetWidth (which include scroll & padding)
let size = Number.parseFloat(elm.style[type]);
if (!size || Number.isNaN(size)) {
const pascalType = type === 'height' ? 'Height' : 'Width';
switch (mode) {
case 'outer':
size = elm[`offset${pascalType}`];
break;
case 'scroll':
size = elm[`scroll${pascalType}`];
break;
case 'inner':
default:
size = elm[`client${pascalType}`];
break;
}
size = elm.getBoundingClientRect()[type];
}
if (!size || Number.isNaN(size)) {
// when 0 width, we'll try different ways
// when element is auto or 0, we'll keep previous style values to get width and then reapply original values
const prevDisplay = elm.style.display;
const prevPosition = elm.style.position;
elm.style.display = 'block';
elm.style.position = 'absolute';
size = getComputedSize(elm, type);
// reapply original values
elm.style.display = prevDisplay;
elm.style.position = prevPosition;
}
return size || 0;
}
/**
* use `getComputedStyle()` and return size has a number or default to 0
* @param elm - HTML element
* @param styleType - CSS style type (e.g. 'width', 'paddingLeft', ...)
* @returns - return size as a number or 0 when unparseable
*/
export function getComputedSize(elm: HTMLElement, styleType: string) {
const elmStyle = window.getComputedStyle(elm)[styleType as any];
let size = Number.parseFloat(elmStyle);
if (Number.isNaN(size)) {
size = 0;
}
return size;
}
/**
* Find a single parent by a simple selector, it only works with a simple selector
* for example: "input.some-class", ".some-class", "input#some-id"
* Note: it won't work with complex selector like "div.some-class input.my-class"
* @param elm - HTML element
* @param selector - HTML selector
* @returns
*/
export function findParent(elm: HTMLElement, selector: string) {
let targetElm: HTMLElement | null = null;
let parentElm = elm?.parentElement;
while (parentElm) {
// query selector id (#some-id) or class (.some-class other-class)
const [_, nodeType, selectorType, classOrIdName] = selector.match(/^([a-z]*)([#.]{1})([a-z-]+)$/i) || [];
if (selectorType && classOrIdName) {
// class or id selector type
for (const q of classOrIdName.replace(selectorType, '').split(' ')) {
if (parentElm.classList.contains(q)) {
if (nodeType) {
if (parentElm?.tagName.toLowerCase() === nodeType) {
targetElm = parentElm;
}
} else {
targetElm = parentElm;
}
}
}
}
parentElm = parentElm.parentElement;
}
return targetElm;
}
/**
* Simple function to decode the most common HTML entities.
* For example: "<div>Hablar español? </div>" => "Hablar español? 🦄
"
* @param {String} inputValue - input value to be decoded
* @return {String}
*/
export function htmlDecode(input?: string | boolean | number): string {
if (isDefined(input)) {
// 1. decode html entities (e.g. `'` => single quote)
// 2. use textarea to decode the rest (e.g. html tags and symbols, `<div>` => ``)
const txt = document.createElement('textarea');
txt.innerHTML = input.toString().replace(/(\d+);/g, (_, dec) => String.fromCharCode(dec));
return txt.value;
}
return '';
}
export function insertAfter(referenceNode: HTMLElement, newNode: HTMLElement) {
referenceNode.parentNode?.insertBefore(newNode, referenceNode.nextSibling);
}
export function omitProp(obj: any, key: string) {
const { [key]: _omitted, ...rest } = obj;
return rest;
}
/** Display or hide matched element */
export function toggleElement(elm?: HTMLElement | null, display?: boolean) {
if (elm?.style) {
elm.style.display = (elm.style.display === 'none' && display !== false) || display === true ? 'block' : 'none';
}
}
/**
* Get the Window Scroll top/left Position
* @returns
*/
export function windowScrollPosition(): { left: number; top: number } {
return {
left: window.pageXOffset || document.documentElement.scrollLeft || 0,
top: window.pageYOffset || document.documentElement.scrollTop || 0,
};
}