import { toRef, watch } from 'vue'; import type { ComputedRef, MaybeRef, MaybeRefOrGetter, Ref } from 'vue'; import { isString } from '@robonen/stdlib'; import { defaultDocument } from '@/types'; import type { ConfigurableDocument } from '@/types'; export interface UseFaviconOptions extends ConfigurableDocument { /** * Base URL prepended to the icon href * * @default '' */ baseUrl?: string; /** * The `rel` attribute of the favicon link element * * @default 'icon' */ rel?: string; } export type UseFaviconReturn = ComputedRef | Ref; // Matches a real file extension at the very end of the path portion of the href, // e.g. `/foo.png` -> `png`, but NOT `/foo.png?v=2`, `data:...`, or extensionless hrefs. const FILE_EXTENSION_RE = /\.([a-z0-9]+)$/i; /** * @name useFavicon * @category Browser * @description Reactive favicon. * * @param {MaybeRefOrGetter} [newIcon] Initial icon href. A getter or readonly ref yields a read-only `ComputedRef`; a writable ref or plain value yields a writable `Ref`. * @param {UseFaviconOptions} [options={}] Options * @returns {UseFaviconReturn} A ref bound to the favicon href * * @example * const favicon = useFavicon(); * favicon.value = '/new-icon.png'; * * @example * // Track an existing reactive source (read-only result) * const isDark = useDark(); * const favicon = useFavicon(() => isDark.value ? '/dark.png' : '/light.png'); * * @since 0.0.15 */ export function useFavicon( newIcon: MaybeRefOrGetter, options?: UseFaviconOptions, ): ComputedRef; export function useFavicon( newIcon?: MaybeRef, options?: UseFaviconOptions, ): Ref; export function useFavicon( newIcon: MaybeRefOrGetter = null, options: UseFaviconOptions = {}, ): UseFaviconReturn { const { baseUrl = '', rel = 'icon', document = defaultDocument, } = options; const favicon = toRef(newIcon); const selector = `link[rel*="${rel}"]`; const applyIcon = (icon: string) => { if (!document) return; const href = `${baseUrl}${icon}`; const elements = document.head.querySelectorAll(selector); if (!elements.length) { const link = document.createElement('link'); link.rel = rel; link.href = href; // Only set a MIME type when the icon actually ends in a file extension; // otherwise we'd emit garbage like `image/png?v=2` or `image/` for // query-string, extensionless, or data: hrefs. const extension = FILE_EXTENSION_RE.exec(icon)?.[1]; if (extension) link.type = `image/${extension}`; document.head.append(link); return; } for (const element of elements) element.href = href; }; watch( favicon, (icon, oldIcon) => { if (isString(icon) && icon !== oldIcon) applyIcon(icon); }, { immediate: true }, ); return favicon; }