@oslokommune/punkt-elements
Version:
Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo
141 lines (119 loc) • 4.19 kB
text/typescript
import { PktElement } from '@/base-elements/element'
import { ElementProps } from '@/types/typeUtils'
import { PktIconName } from '@oslokommune/punkt-assets/dist/icons/icon'
import { html, PropertyValues } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
const defaultPath = 'https://punkt-cdn.oslo.kommune.no/latest/icons/'
// Allow global override of icon fetch
if (typeof window !== 'undefined') {
window.pktFetch = window.pktFetch === undefined ? fetch : window.pktFetch
window.pktIconPath = window.pktIconPath || defaultPath
}
const errorSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"></svg>'
const dlCache: { [key: string]: Promise<string> } = {}
const MAX_RETRIES = 2
const RETRY_DELAY = 1500
const isSessionStorageAvailable =
typeof Storage !== 'undefined' && typeof sessionStorage !== 'undefined'
const fetchIcon = (url: string): Promise<string> =>
window
.pktFetch!(url)
.then((response: Response) => {
if (!response.ok) {
throw new Error('Missing icon: ' + url)
}
return response.text()
})
const fetchIconWithRetry = async (url: string, retries = MAX_RETRIES): Promise<string> => {
try {
return await fetchIcon(url)
} catch (error) {
if (retries > 0) {
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY))
return fetchIconWithRetry(url, retries - 1)
}
// eslint-disable-next-line no-console
console.error('Failed to fetch icon: ' + url)
return errorSvg
}
}
const downloadIconOrGetFromCache = async (
name: PktIconName,
path: string | undefined,
): Promise<string | null> => {
const key = path + name + '.svg'
if (isSessionStorageAvailable && sessionStorage.getItem(key)) {
return sessionStorage.getItem(key)
}
// If a fetch is already in-flight for this icon, await the same promise
if (key in dlCache) {
return dlCache[key]
}
if (typeof window !== 'undefined' && typeof window.pktFetch === 'function') {
dlCache[key] = fetchIconWithRetry(key).then((text) => {
if (text !== errorSvg && isSessionStorageAvailable) {
sessionStorage.setItem(key, text)
}
delete dlCache[key]
return text
})
return dlCache[key]
}
return errorSvg
}
type Props = ElementProps<PktIcon, 'path' | 'name'>
export class PktIcon extends PktElement<Props> {
({ type: String, reflect: false })
path: string | undefined = typeof window !== 'undefined' ? window.pktIconPath : defaultPath
({ type: String, reflect: true })
name: PktIconName = ''
({ type: SVGElement })
private icon: ReturnType<typeof unsafeSVG> = unsafeSVG(errorSvg)
({ type: Array, noAccessor: true })
private _updatedProps: string[] = []
connectedCallback(): void {
super.connectedCallback()
this.classList.add('pkt-icon')
}
async attributeChangedCallback(name: string, _old: string | null, value: string | null) {
super.attributeChangedCallback(name, _old, value)
if (name === 'name' || name === 'path') this.getIcon(this.name)
}
protected async updated(_changedProperties: PropertyValues) {
super.updated(_changedProperties)
if (_changedProperties.has('name') || _changedProperties.has('path')) {
this.getIcon(this.name)
}
}
protected async getIcon(name: PktIconName = '') {
if (this._updatedProps.length > 0) {
if (!this.path) this.path = typeof window !== 'undefined' ? window.pktIconPath : defaultPath
try {
this.icon = unsafeSVG(
await downloadIconOrGetFromCache(this.name || '', this.path),
) as SVGElement
} catch {
this.icon = unsafeSVG(errorSvg)
}
this._updatedProps = []
} else {
if (!this._updatedProps.includes(name)) {
this._updatedProps.push(name)
}
}
}
render() {
return html`${this.name && this.icon}`
}
}
declare global {
interface HTMLElementTagNameMap {
'pkt-icon': PktIcon
}
}
try {
customElement('pkt-icon')(PktIcon)
} catch (e) {
console.warn('Forsøker å definere <pkt-icon>, men den er allerede definert')
}