mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
Finish rewrite
This commit is contained in:
@@ -97,12 +97,10 @@ export default class WaIcon extends WebAwesomeElement {
|
||||
}
|
||||
|
||||
private getIconSource(): string | undefined {
|
||||
if (this.src) {
|
||||
return this.src;
|
||||
}
|
||||
let ref = this.src ?? this.name;
|
||||
|
||||
if (this.name) {
|
||||
return this.iconLibrary?.getUrl(this.name, this.family, this.variant);
|
||||
if (ref) {
|
||||
return this.iconLibrary?.getUrl(ref, this.family, this.variant);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -133,14 +131,17 @@ export default class WaIcon extends WebAwesomeElement {
|
||||
async setIcon() {
|
||||
this.iconLibrary ??= getIconLibrary(this.src ? 'custom' : this.library);
|
||||
|
||||
const url = this.getIconSource();
|
||||
let { src, name, family, variant } = this;
|
||||
let ref = src ?? name;
|
||||
|
||||
if (!url || !this.iconLibrary) {
|
||||
const url = ref ? this.iconLibrary?.getUrl(ref, family, variant) : undefined;
|
||||
|
||||
if (!ref || !url || !this.iconLibrary) {
|
||||
this.svg = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let iconResolver = this.iconLibrary.getElement(url);
|
||||
let iconResolver = this.iconLibrary.getElement(ref, family, variant);
|
||||
|
||||
// If we haven't rendered yet, exit early. This avoids unnecessary work due to watching multiple props.
|
||||
if (!this.initialRender) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IconLibraryCache, UnregisteredIconLibrary } from './library.js';
|
||||
import type { IconLibraryCacheDeep, UnregisteredIconLibrary } from './library.js';
|
||||
|
||||
let kitCode = '';
|
||||
|
||||
@@ -80,7 +80,7 @@ function getIconUrl(name: string, family: string, variant: string) {
|
||||
}
|
||||
|
||||
// Icons used for internal components, prefetched for speed
|
||||
export const fetched: IconLibraryCache<2> = {
|
||||
export const inlined: IconLibraryCacheDeep = {
|
||||
classic: {
|
||||
solid: {
|
||||
check: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>`,
|
||||
@@ -110,8 +110,9 @@ export const fetched: IconLibraryCache<2> = {
|
||||
const library: UnregisteredIconLibrary = {
|
||||
name: 'default',
|
||||
getUrl: getIconUrl,
|
||||
getKey: url => url.replace(/\?token=[^&]+/, ''),
|
||||
inlined: fetched,
|
||||
// Cache icons using the free URL
|
||||
getCacheKey: url => url.replace(/\?token=[^&]+/, '').replace(/ka-p\./, 'ka-f.'),
|
||||
inlined,
|
||||
};
|
||||
|
||||
export default library;
|
||||
|
||||
@@ -3,7 +3,9 @@ export const CACHEABLE_ERROR = Symbol('CACHEABLE_ERROR');
|
||||
export const RETRYABLE_ERROR = Symbol('RETRYABLE_ERROR');
|
||||
|
||||
// 410: Gone
|
||||
export const CACHEABLE_HTTP_ERRORS = [403, 404, 410];
|
||||
// NOTE: Resist the temptation to add 403 and 404 to this list.
|
||||
// We may get them before a token is added, and we need to be able to retry.
|
||||
export const CACHEABLE_HTTP_ERRORS = [410];
|
||||
|
||||
let parser: DOMParser;
|
||||
|
||||
@@ -13,15 +15,14 @@ export default class IconLibrary {
|
||||
|
||||
readonly name: string;
|
||||
readonly mutator?: IconLibraryMutator;
|
||||
readonly getKey?: IconLibraryGetKey;
|
||||
readonly system?: IconLibrarySystemResolver;
|
||||
readonly system?: IconMapping;
|
||||
readonly spriteSheet?: boolean;
|
||||
|
||||
/** Inlined markup, keyed by URL */
|
||||
inlined: IconLibraryCache<0> = {};
|
||||
inlined: IconLibraryCacheFlat = {};
|
||||
|
||||
/** DOM nodes, keyed by URL */
|
||||
elements: Record<string, SVGElement | typeof CACHEABLE_ERROR | typeof RETRYABLE_ERROR> = {};
|
||||
cache: Record<string, SVGElement | typeof CACHEABLE_ERROR | typeof RETRYABLE_ERROR> = {};
|
||||
|
||||
constructor(library: UnregisteredIconLibrary) {
|
||||
// Store library definition
|
||||
@@ -30,7 +31,6 @@ export default class IconLibrary {
|
||||
// Copy certain properties
|
||||
this.name = library.name;
|
||||
this.mutator = library.mutator;
|
||||
this.getKey = library.getKey;
|
||||
this.system = library.system;
|
||||
this.spriteSheet = library.spriteSheet;
|
||||
|
||||
@@ -42,19 +42,31 @@ export default class IconLibrary {
|
||||
/**
|
||||
* Convert an icon name, family, and variant into a URL
|
||||
*/
|
||||
getUrl(name: string, family: string, variant: string) {
|
||||
getUrl(name: string, family?: string, variant?: string) {
|
||||
// console.warn('getUrl', name, family, variant);
|
||||
if (name.startsWith('system:')) {
|
||||
name = name.slice(7);
|
||||
|
||||
if (this.system) {
|
||||
let resolved = this.system(name, family, variant);
|
||||
name = resolved.name ?? name;
|
||||
family = resolved.family ?? family;
|
||||
variant = resolved.variant ?? variant;
|
||||
|
||||
if (resolved) {
|
||||
name = resolved.name ?? name;
|
||||
family = resolved.family ?? family;
|
||||
variant = resolved.variant ?? variant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.spec.getUrl?.(name, family, variant);
|
||||
if (this.spec.getUrl) {
|
||||
return this.spec.getUrl(name, family, variant);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
getCacheKey(url: string) {
|
||||
return this.spec.getCacheKey?.(url) ?? url;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,12 +77,14 @@ export default class IconLibrary {
|
||||
return `<svg><use part="use" href="${url}"></use></svg>`;
|
||||
}
|
||||
|
||||
let cacheKey = this.getKey?.(url) ?? url;
|
||||
let markup: string | typeof CACHEABLE_ERROR | undefined = this.inlined?.[cacheKey];
|
||||
let cacheKey = this.getCacheKey(url);
|
||||
let markup = this.inlined[cacheKey];
|
||||
|
||||
if (!markup) {
|
||||
return fetchIcon(url).then(markup => {
|
||||
if (typeof markup === 'string' || markup === CACHEABLE_ERROR) {
|
||||
if (typeof markup === 'string') {
|
||||
// TBD: Should we add to inlined? DOM nodes are cached anyway, perhaps that’s enough?
|
||||
// Or perhaps we should go the other way and cache CACHEABLE_ERROR too?
|
||||
this.inlined[cacheKey] = markup;
|
||||
}
|
||||
|
||||
@@ -82,19 +96,51 @@ export default class IconLibrary {
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL, this function returns the resulting SVG element or an appropriate error symbol.
|
||||
* Given a name, family, and variant, this function returns the resulting SVG element or an appropriate error symbol.
|
||||
* If the icon library defines fallbacks, they will be tried in order.
|
||||
*/
|
||||
async getElement(url: string): Promise<SVGElement | typeof CACHEABLE_ERROR | typeof RETRYABLE_ERROR> {
|
||||
if (this.elements[url]) {
|
||||
return this.elements[url];
|
||||
async getElement(
|
||||
name: string,
|
||||
family?: string,
|
||||
variant?: string,
|
||||
): Promise<SVGElement | typeof CACHEABLE_ERROR | typeof RETRYABLE_ERROR> {
|
||||
let url = this.getUrl(name, family, variant);
|
||||
let cacheKey = this.getCacheKey(url);
|
||||
|
||||
if (this.cache[cacheKey]) {
|
||||
return this.cache[cacheKey];
|
||||
}
|
||||
|
||||
let markup = await this.getMarkup(url);
|
||||
let result;
|
||||
|
||||
if (markup === CACHEABLE_ERROR || markup === RETRYABLE_ERROR) {
|
||||
return markup;
|
||||
result = markup;
|
||||
} else {
|
||||
result = await this.getElementFromMarkup(markup);
|
||||
}
|
||||
|
||||
if (result === CACHEABLE_ERROR || result === RETRYABLE_ERROR) {
|
||||
if (this.spec.fallback) {
|
||||
// Try again with fallback
|
||||
let fallback = this.spec.fallback(name, family, variant);
|
||||
if (fallback) {
|
||||
return this.getElement(fallback.name, fallback.family, fallback.variant);
|
||||
}
|
||||
}
|
||||
|
||||
if (result === CACHEABLE_ERROR) {
|
||||
this.cache[cacheKey] = result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL, this function synchronously returns the resulting SVG element or an appropriate error symbol.
|
||||
*/
|
||||
getElementFromMarkup(markup: string): SVGElement | typeof CACHEABLE_ERROR | typeof RETRYABLE_ERROR {
|
||||
let svgEl;
|
||||
try {
|
||||
const div = document.createElement('div');
|
||||
@@ -121,20 +167,14 @@ export default class IconLibrary {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const result = svgEl ?? CACHEABLE_ERROR;
|
||||
this.elements[url] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
fallback(url: string) {
|
||||
// TODO implement this
|
||||
return svgEl ?? CACHEABLE_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the deep family → variant → icon name → markup cache that is more convenient to write out manually
|
||||
* to the flat URL → markup cache that icon libraries use internally
|
||||
**/
|
||||
inline(cache: IconLibraryFetched) {
|
||||
inline(cache: IconLibraryCacheDeep) {
|
||||
// If no getUrl function was provided, this library does not use names,
|
||||
// so this should already be a flat URL → markup mapping
|
||||
let flatCache = cache;
|
||||
@@ -148,55 +188,43 @@ export default class IconLibrary {
|
||||
let name = path.pop()!;
|
||||
let [family, variant] = path;
|
||||
let url = this.getUrl(name as string, family as string, variant as string);
|
||||
let key = this.getKey?.(url!) ?? url;
|
||||
return key as string;
|
||||
return this.getCacheKey(url!);
|
||||
},
|
||||
}) as IconLibraryCache<0>;
|
||||
}) as IconLibraryCacheFlat;
|
||||
}
|
||||
|
||||
Object.assign(this.inlined, flatCache);
|
||||
}
|
||||
}
|
||||
|
||||
export type IconLibraryResolver = (name: string, family: string, variant: string) => string;
|
||||
export type IconLibrarySystemResolver = (
|
||||
export type IconLibraryResolver = (name: string, family?: string, variant?: string) => string;
|
||||
export type IconMapping = (
|
||||
name: string,
|
||||
family: string,
|
||||
variant: string,
|
||||
) => { name: string; family?: string; variant?: string };
|
||||
family?: string,
|
||||
variant?: string,
|
||||
) => { name: string; family?: string; variant?: string; library?: string } | undefined;
|
||||
export type IconLibraryGetKey = (name: string) => string;
|
||||
export type IconLibraryMutator = (svg: SVGElement) => void;
|
||||
export type IconFetchedResult = string | typeof CACHEABLE_ERROR | typeof RETRYABLE_ERROR;
|
||||
|
||||
// This is a utility for decrementing a number up to 3 by one
|
||||
// e.g., Decrement[3] yields 2
|
||||
type Decrement = [never, 0, 1, 2];
|
||||
|
||||
/**
|
||||
* Record of string → string or nested string → Record<string, ...>
|
||||
* The number indicates how many params we have; none (0), just family (1), or family and style (2).
|
||||
* IconLibraryCache<0> is Record<string, string> (name → markup)
|
||||
* IconLibraryCache<1> is Record<string, Record<string, string>> (family → name → markup)
|
||||
* IconLibraryCache<2> is Record<string, Record<string, Record<string, string>>> (family → variant → name → markup)
|
||||
*/
|
||||
export type IconLibraryCache<N extends number = 0> = Record<
|
||||
string,
|
||||
N extends 0 ? string | typeof CACHEABLE_ERROR : IconLibraryCache<Decrement[N]>
|
||||
>;
|
||||
|
||||
export type IconLibraryFetched = IconLibraryCache<0> | IconLibraryCache<1> | IconLibraryCache<2>;
|
||||
export type IconLibraryCacheFlat = Record<string, string>;
|
||||
export type IconLibraryCacheDeep =
|
||||
| IconLibraryCacheFlat
|
||||
| Record<string, IconLibraryCacheFlat>
|
||||
| Record<string, Record<string, IconLibraryCacheFlat>>;
|
||||
|
||||
export interface UnregisteredIconLibrary {
|
||||
name: string;
|
||||
getUrl?: IconLibraryResolver;
|
||||
system?: IconLibrarySystemResolver;
|
||||
system?: IconMapping;
|
||||
fallback?: IconMapping;
|
||||
mutator?: IconLibraryMutator;
|
||||
getKey?: IconLibraryGetKey;
|
||||
getCacheKey?: IconLibraryGetKey;
|
||||
spriteSheet?: boolean;
|
||||
|
||||
// Max depth: family → variant → icon name → markup
|
||||
// but may be shallower for libraries that don't use variants or families
|
||||
inlined?: IconLibraryFetched;
|
||||
inlined?: IconLibraryCacheDeep;
|
||||
}
|
||||
|
||||
export async function fetchIcon(url: string) {
|
||||
@@ -208,7 +236,7 @@ export async function fetchIcon(url: string) {
|
||||
}
|
||||
|
||||
return fileData.text();
|
||||
} catch {
|
||||
} catch (e) {
|
||||
return RETRYABLE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ import IconLibrary, {
|
||||
RETRYABLE_ERROR,
|
||||
fetchIcon,
|
||||
type IconFetchedResult,
|
||||
type IconLibraryCache,
|
||||
type IconLibraryFetched,
|
||||
type IconLibraryCacheDeep,
|
||||
type IconLibraryCacheFlat,
|
||||
type UnregisteredIconLibrary,
|
||||
} from './library.js';
|
||||
|
||||
export { CACHEABLE_ERROR, RETRYABLE_ERROR, fetchIcon };
|
||||
export type { IconFetchedResult, IconLibrary, IconLibraryCache, IconLibraryFetched, UnregisteredIconLibrary };
|
||||
export type { IconFetchedResult, IconLibrary, IconLibraryCacheDeep, IconLibraryCacheFlat, UnregisteredIconLibrary };
|
||||
|
||||
let registry: IconLibrary[] = [];
|
||||
let watchedIcons: WaIcon[] = [];
|
||||
|
||||
Reference in New Issue
Block a user