nanosplash
Version:
The tiny loading screen for web artisans
155 lines (140 loc) • 4.53 kB
text/typescript
import style from '@/style/ns.css?inline'
import type { INanosplash } from '@/interfaces/INanosplash.ts'
import type { Func } from '@/types/generic/function.ts'
import type { INSElement } from '@/interfaces/INSElement.ts'
import {
all,
bod,
div,
doc,
first,
parseHtml,
parseRef,
toArray,
} from '@/utils/dom-utils.ts'
import { ClassNames, Selectors } from '@/constants/ns.ts'
import type { HTMLString } from '@/types/semantic/string.ts'
import type { ElementRef } from '@/types/dom.ts'
import type { int } from '@/types/semantic/number.ts'
import { version } from '@/../package.json'
export const useNs: Func<INanosplash> = (): INanosplash => {
/**
* Retrieve all NS elements from the DOM
*/
const getAllNs: Func<INSElement[]> = () =>
all(doc(), Selectors.ns) as INSElement[]
/**
* Create an NS element
*/
const makeNs: Func<INSElement> = () => {
const circle: HTMLString =
'<circle class=path cx=25 cy=25 r=20 fill=none />'
const svg = parseHtml(`<svg viewBox="0 0 50 50">${circle}</svg>`) as Element
const node = div(
ClassNames.ns,
div(ClassNames.nsText),
div(ClassNames.nsSpinner, svg)
) as INSElement
node.nsId = Date.now()
return node
}
/**
* Get all NS elements from DOM in a FIFO sequence
*/
const nsQueue: Func<INSElement[]> = () =>
getAllNs().sort((a, b) => a.nsId - b.nsId)
/**
* Peek at the first NS element in the queue
*/
const peekNsQueue: Func<INSElement | null> = () => nsQueue()[0] ?? null
/**
* Set the text of the NS element.
* @param ns Nanosplash element.
* @param text The text to display adjacent to the spinner.
*/
const setNsText = (ns: INSElement, text?: string): void => {
first(ns, Selectors.nsText)?.remove()
if (!text) return
const newNsText: Element = div(ClassNames.nsText, text)
ns.insertBefore(newNsText, ns.firstChild)
}
/**
* Set the parent element of the NS element.
* @param ns Nanosplash element.
* @param parent An element to be a parent of the Nanosplash element.
*/
const setNsParent = (ns: INSElement, parent: Element): void => {
const child = parent.firstElementChild
if (child) parent.insertBefore(ns, child)
parent.append(ns)
parent.classList.add(ClassNames.nsHost)
}
const getNsInside = (parent: Element): INSElement | undefined => {
const children = toArray(parent.children) as INSElement[]
return children.find((v: Element): boolean =>
v.classList.contains(ClassNames.ns)
)
}
/**
* Show the Nanosplash in the window
* @param text The text that will appear adjacent to the spinner. Enter false to only show the spinner.
* @param inside Element or selector to an element wherein the Nanosplash element should reside.
*/
const show = (text?: string, inside?: ElementRef): int | null => {
const parent: Element = inside ? (parseRef(inside) ?? bod()) : bod()
let ns: INSElement
const recycled = getNsInside(parent)
if (recycled) {
ns = recycled as INSElement
} else {
ns = makeNs()
setNsParent(ns, parent)
}
setNsText(ns, text ?? '')
if (parent === bod()) {
const top: string = scrollY + 'px'
bod().style.setProperty('--ns-top', top)
}
return ns.nsId
}
/**
* Remove a Nanosplash element from the DOM
* @param ns The Nanosplash element
*/
const removeNs = (ns?: Element | INSElement | null): void => {
ns?.parentElement?.classList.remove(ClassNames.nsHost)
ns?.remove()
}
/**
* Find a Nanosplash element with a given ID
* @param id The Nanosplash ID
*/
const findNs = (id: int): INSElement | null =>
getAllNs().find(x => x.nsId === id) ?? null
/**
* Hide agnostically in FIFO order, specifically by ID, or all, by the asterisk symbol.
* The latter will delete all Nanosplash elements in the DOM.
* @param id Optional ID of a Nanosplash element or '*'.
*/
const hide = (id?: int | '*'): void => {
const selectAll = id === '*'
if (selectAll) getAllNs().forEach(removeNs)
else removeNs(typeof id === 'number' ? findNs(id) : peekNsQueue())
}
/**
* Remove old Nanosplash styles and insert new ones.
*/
const injectStyle = () => {
first(doc(), '#ns')?.remove()
const styleElement: HTMLStyleElement = parseHtml(
`<style id="ns">${style}</style>`
)
bod().append(styleElement)
}
injectStyle() // Run everytime the hook is invoked.
return {
show,
hide,
version,
}
}