import { CodedError, registerWebModule } from 'expo-modules-core'; import FontObserver from 'fontfaceobserver'; import { FontDisplay } from './Font.types'; function getFontFaceStyleSheet() { if (typeof window === 'undefined') { return null; } const styleSheet = getStyleElement(); return styleSheet.sheet ? styleSheet.sheet : null; } function getFontFaceRules() { const sheet = getFontFaceStyleSheet(); if (sheet) { // @ts-ignore: rule iterator const rules = [...sheet.cssRules]; const items = []; for (let i = 0; i < rules.length; i++) { const rule = rules[i]; if (rule instanceof CSSFontFaceRule) { items.push({ rule, index: i }); } } return items; } return []; } function getFontFaceRulesMatchingResource(fontFamilyName, options) { const rules = getFontFaceRules(); return rules.filter(({ rule }) => { return (rule.style.fontFamily === fontFamilyName && (options && options.display ? options.display === rule.style.fontDisplay : true)); }); } const serverContext = new Set(); function getHeadElements() { const entries = [...serverContext.entries()]; if (!entries.length) { return []; } const css = entries.map(([{ css }]) => css).join('\n'); const links = entries.map(([{ resourceId }]) => resourceId); // TODO: Maybe return nothing if no fonts were loaded. return [ { $$type: 'style', children: css, id: ID, type: 'text/css', }, ...links.map((resourceId) => ({ $$type: 'link', rel: 'preload', href: resourceId, as: 'font', crossorigin: '', })), ]; } const ExpoFontLoader = { async unloadAllAsync() { if (typeof window === 'undefined') return; const element = document.getElementById(ID); if (element && element instanceof HTMLStyleElement) { document.removeChild(element); } }, async unloadAsync(fontFamilyName, options) { const sheet = getFontFaceStyleSheet(); if (!sheet) return; const items = getFontFaceRulesMatchingResource(fontFamilyName, options); for (const item of items) { sheet.deleteRule(item.index); } }, getServerResources() { const elements = getHeadElements(); return elements .map((element) => { switch (element.$$type) { case 'style': return ``; case 'link': return ``; default: return ''; } }) .filter(Boolean); }, resetServerContext() { serverContext.clear(); }, getLoadedFonts() { if (typeof window === 'undefined') { return [...serverContext.values()].map(({ name }) => name); } const rules = getFontFaceRules(); return rules.map(({ rule }) => rule.style.fontFamily); }, isLoaded(fontFamilyName, resource = {}) { if (typeof window === 'undefined') { return !![...serverContext.values()].find((asset) => { return asset.name === fontFamilyName; }); } return getFontFaceRulesMatchingResource(fontFamilyName, resource)?.length > 0; }, // NOTE(vonovak): This is used in RN vector-icons to load fonts dynamically on web. Changing the signature is breaking. // NOTE(EvanBacon): No async keyword! This cannot return a promise in Node environments. loadAsync(fontFamilyName, resource) { if (__DEV__ && typeof resource !== 'object') { // to help devving on web, where loadAsync interface is different from native throw new CodedError('ERR_FONT_SOURCE', `Expected font resource of type \`object\` instead got: ${typeof resource}`); } if (typeof window === 'undefined') { serverContext.add({ name: fontFamilyName, css: _createWebFontTemplate(fontFamilyName, resource), // @ts-expect-error: typeof string resourceId: resource.uri, }); return Promise.resolve(); } const canInjectStyle = document.head && typeof document.head.appendChild === 'function'; if (!canInjectStyle) { throw new CodedError('ERR_WEB_ENVIRONMENT', `The browser's \`document.head\` element doesn't support injecting fonts.`); } const style = getStyleElement(); document.head.appendChild(style); const res = getFontFaceRulesMatchingResource(fontFamilyName, resource); if (!res.length) { _createWebStyle(fontFamilyName, resource); } if (!isFontLoadingListenerSupported()) { return Promise.resolve(); } return new FontObserver(fontFamilyName, { // @ts-expect-error: TODO(@kitten): Typings indicate that the polyfill may not support this? display: resource.display, }).load(resource.testString ?? null, 12000); }, }; const isServer = process.env.EXPO_OS === 'web' && typeof window === 'undefined'; function createExpoFontLoader() { return ExpoFontLoader; } const toExport = isServer ? ExpoFontLoader : // @ts-expect-error: registerWebModule calls `new` on the module implementation. // Normally that'd be a class but that doesn't work on server, so we use a function instead. // TS doesn't like that but we don't need it to be a class. registerWebModule(createExpoFontLoader, 'ExpoFontLoader'); export default toExport; const ID = 'expo-generated-fonts'; function getStyleElement() { const element = document.getElementById(ID); if (element && element instanceof HTMLStyleElement) { return element; } const styleElement = document.createElement('style'); styleElement.id = ID; return styleElement; } export function _createWebFontTemplate(fontFamily, resource) { return `@font-face{font-family:"${fontFamily}";src:url("${resource.uri}");font-display:${resource.display || FontDisplay.AUTO}}`; } function _createWebStyle(fontFamily, resource) { const fontStyle = _createWebFontTemplate(fontFamily, resource); const styleElement = getStyleElement(); // @ts-ignore: TypeScript does not define HTMLStyleElement::styleSheet. This is just for IE and // possibly can be removed if it's unnecessary on IE 11. if (styleElement.styleSheet) { const styleElementIE = styleElement; styleElementIE.styleSheet.cssText = styleElementIE.styleSheet.cssText ? styleElementIE.styleSheet.cssText + fontStyle : fontStyle; } else { const textNode = document.createTextNode(fontStyle); styleElement.appendChild(textNode); } return styleElement; } function isFontLoadingListenerSupported() { const { userAgent } = window.navigator; // WebKit is broken https://github.com/bramstein/fontfaceobserver/issues/95 const isIOS = !!userAgent.match(/iPad|iPhone/i); const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // Edge is broken https://github.com/bramstein/fontfaceobserver/issues/109#issuecomment-333356795 const isEdge = userAgent.includes('Edge'); // Internet Explorer const isIE = userAgent.includes('Trident'); return !isSafari && !isIOS && !isEdge && !isIE; } //# sourceMappingURL=ExpoFontLoader.web.js.map