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 = ''
const dlCache: { [key: string]: Promise } = {}
const MAX_RETRIES = 2
const RETRY_DELAY = 1500
const isSessionStorageAvailable =
typeof Storage !== 'undefined' && typeof sessionStorage !== 'undefined'
const fetchIcon = (url: string): Promise =>
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 => {
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 => {
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
export class PktIcon extends PktElement {
@property({ type: String, reflect: false })
path: string | undefined = typeof window !== 'undefined' ? window.pktIconPath : defaultPath
@property({ type: String, reflect: true })
name: PktIconName = ''
@property({
type: String,
attribute: 'aria-hidden',
reflect: true,
})
override ariaHidden: string | null = null
@property({
type: String,
attribute: 'aria-label',
reflect: true,
})
override ariaLabel: string | null = null
@property({ type: SVGElement })
private icon: ReturnType = unsafeSVG(errorSvg)
@property({ type: Array, noAccessor: true })
private _updatedProps: string[] = []
private get effectiveAriaHidden(): boolean {
if (this.ariaHidden !== null && this.ariaHidden !== undefined) {
return this.ariaHidden === '' || this.ariaHidden === 'true'
}
return !this.ariaLabel
}
private get effectiveAriaLabel(): string | null {
if (this.effectiveAriaHidden) return null
return this.ariaLabel || this.name || null
}
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)
}
this.applyAriaToSvg()
}
private applyAriaToSvg() {
const svg = this.querySelector('svg')
if (!svg) return
svg.setAttribute('aria-hidden', String(this.effectiveAriaHidden))
const label = this.effectiveAriaLabel
if (label) {
svg.setAttribute('aria-label', label)
} else {
svg.removeAttribute('aria-label')
}
}
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 , men den er allerede definert')
}