Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | 13x 22x 32x 34x 5x 29x 29x 19x 10x 1x 9x 34x 7x 9x 1x 1x 8x 2x 6x 6x 6x 16x 1x 15x 2x 13x 4x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 18x 8x 8x 8x 8x 1x 7x 7x 1x 6x 18x 18x 18x 18x 18x | // SPDX-License-Identifier: MIT
/**
* Lazy theme-CSS loading helpers for showcase-style pages.
*
* Instead of eagerly linking every theme's stylesheet, a page loads only
* the default/active theme CSS up front and fetches the rest on demand.
* These helpers provide the load decision, an inject-once on-demand
* loader built on the package's `loadThemeCSS` pipeline, and a
* hover-triggered prefetch that hides switch latency for elements
* marked with the `data-theme-preview` attribute (e.g. marquee cards).
*/
import { CSS_LINK_ID } from './constants.js';
import { ThemeErrors, logThemeError } from './errors.js';
import type { Unsubscribe } from './integration.js';
import { getBaseUrl, loadThemeCSS, resolveAssetPath, themeLinkId } from './theme-loader.js';
import { getValidThemeIds, isValidThemeId, resolveTheme } from './theme-resolver.js';
/** Attribute whose value names the theme to prefetch on hover/focus. */
export const PREFETCH_TRIGGER_ATTRIBUTE = 'data-theme-preview';
/**
* Builds the element ID of a theme's prefetch link.
*
* @param themeId - Theme whose prefetch link ID to build
* @returns The prefetch link element ID
*/
function themePrefetchLinkId(themeId: string): string {
return `theme-${themeId}-prefetch`;
}
/**
* Checks whether a theme ID is well-formed and registered.
*
* @param themeId - Theme ID to check
* @returns True when the ID names a known theme
*/
function isKnownThemeId(themeId: string): boolean {
return isValidThemeId(themeId) && getValidThemeIds().has(themeId);
}
/**
* Checks whether a theme's stylesheet is already present in the document.
*
* Recognises both links owned by the selector (`theme-<id>-css`) and the
* blocking script's FOUC-prevention link when it points at the theme.
*
* @param documentObj - Document to inspect
* @param themeId - Theme whose stylesheet to look for
* @returns True when the theme's CSS is already linked
*/
export function isThemeCSSLoaded(documentObj: Document, themeId: string): boolean {
if (documentObj.getElementById(themeLinkId(themeId))) {
return true;
}
const blockingLink = documentObj.getElementById(CSS_LINK_ID) as HTMLLinkElement | null;
if (!blockingLink) {
return false;
}
if (blockingLink.getAttribute('data-theme-id') === themeId) {
return true;
}
const href = blockingLink.getAttribute('href') ?? '';
return href.endsWith(`/${themeId}.css`);
}
/**
* Decides whether a theme's CSS needs an on-demand load.
*
* @param documentObj - Document to inspect
* @param themeId - Theme the page is about to switch to
* @returns True when the theme is known and its CSS is not yet linked
*/
export function shouldLoadThemeCSS(documentObj: Document, themeId: string): boolean {
return isKnownThemeId(themeId) && !isThemeCSSLoaded(documentObj, themeId);
}
/**
* Loads a theme's CSS on demand through the selector's loader.
*
* Injects the stylesheet link at most once: repeat calls for a theme
* whose CSS is already linked are no-ops that resolve to true. Unknown
* or malformed theme IDs are rejected without touching the document.
*
* @param documentObj - Document to load the stylesheet into
* @param themeId - Theme whose CSS to load
* @returns True when the CSS is available (loaded now or previously)
*/
export async function loadThemeCSSOnDemand(
documentObj: Document,
themeId: string,
): Promise<boolean> {
if (!isKnownThemeId(themeId)) {
logThemeError(ThemeErrors.INVALID_THEME_ID(themeId));
return false;
}
if (isThemeCSSLoaded(documentObj, themeId)) {
return true;
}
const theme = resolveTheme(themeId);
Iif (!theme) {
return false;
}
// loadThemeCSS reports whether the stylesheet was confirmed loaded,
// including on the adoption path where the blocking link is repointed.
return loadThemeCSS(documentObj, theme, getBaseUrl(documentObj));
}
/**
* Prefetches a theme's CSS so a later switch hits the browser cache.
*
* Injects a `<link rel="prefetch" as="style">` at most once per theme.
* Skips themes whose stylesheet is already linked, themes already
* prefetched, and unknown theme IDs.
*
* @param documentObj - Document to inject the prefetch link into
* @param themeId - Theme whose CSS to prefetch
* @returns True when a prefetch link was injected by this call
*/
export function prefetchThemeCSS(documentObj: Document, themeId: string): boolean {
if (!isKnownThemeId(themeId)) {
return false;
}
if (isThemeCSSLoaded(documentObj, themeId)) {
return false;
}
if (documentObj.getElementById(themePrefetchLinkId(themeId))) {
return false;
}
const theme = resolveTheme(themeId);
Iif (!theme) {
return false;
}
let href: string;
try {
href = resolveAssetPath(theme.cssFile, getBaseUrl(documentObj));
} catch {
logThemeError(ThemeErrors.INVALID_CSS_PATH(themeId));
return false;
}
const link = documentObj.createElement('link');
link.id = themePrefetchLinkId(themeId);
link.rel = 'prefetch';
link.setAttribute('as', 'style');
link.href = href;
link.setAttribute('data-theme-prefetch', themeId);
documentObj.head.appendChild(link);
return true;
}
/**
* Wires hover/focus-triggered CSS prefetch for marked elements.
*
* Listens for `mouseover` and `focusin` on the document and prefetches
* the theme named by the nearest ancestor carrying the
* `data-theme-preview` attribute. Delegation means marquee items added
* after wiring are covered without re-wiring.
*
* @param documentObj - Document to listen on (defaults to global)
* @returns Function that removes the listeners when called
*/
export function wireHoverPrefetch(documentObj: Document = document): Unsubscribe {
const handler = (event: Event): void => {
const target = event.target;
Iif (!(target instanceof Element)) {
return;
}
const trigger = target.closest(`[${PREFETCH_TRIGGER_ATTRIBUTE}]`);
if (!trigger) {
return;
}
const themeId = trigger.getAttribute(PREFETCH_TRIGGER_ATTRIBUTE);
if (!themeId) {
return;
}
prefetchThemeCSS(documentObj, themeId);
};
documentObj.addEventListener('mouseover', handler);
documentObj.addEventListener('focusin', handler);
return () => {
documentObj.removeEventListener('mouseover', handler);
documentObj.removeEventListener('focusin', handler);
};
}
|