diff --git a/packages/block-editor/src/components/editor-styles/index.js b/packages/block-editor/src/components/editor-styles/index.js index a59ac310bcd303..8af3b493e44966 100644 --- a/packages/block-editor/src/components/editor-styles/index.js +++ b/packages/block-editor/src/components/editor-styles/index.js @@ -18,6 +18,7 @@ import { useSelect } from '@wordpress/data'; import transformStyles from '../../utils/transform-styles'; import { store as blockEditorStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import useEditorFontsResolver from '../use-editor-fonts-resolver'; extend( [ namesPlugin, a11yPlugin ] ); @@ -105,6 +106,9 @@ function EditorStyles( { styles, scope, transformOptions } ) { ) ) } diff --git a/packages/block-editor/src/components/use-editor-fonts-resolver/index.js b/packages/block-editor/src/components/use-editor-fonts-resolver/index.js new file mode 100644 index 00000000000000..a63111614b23ec --- /dev/null +++ b/packages/block-editor/src/components/use-editor-fonts-resolver/index.js @@ -0,0 +1,71 @@ +/** + * WordPress dependencies + */ +import { useState, useMemo, useCallback } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { getDisplaySrcFromFontFace, loadFontFaceInBrowser } from './utils'; +import { store as editorStore } from '../../store'; + +function useEditorFontsResolver() { + const [ loadedFontUrls, setLoadedFontUrls ] = useState( new Set() ); + + const { currentTheme = {}, fontFamilies = [] } = useSelect( ( select ) => { + return { + currentTheme: + select( editorStore ).getSettings()?.__experimentalFeatures + ?.currentTheme, + fontFamilies: + select( editorStore ).getSettings()?.__experimentalFeatures + ?.typography?.fontFamilies, + }; + }, [] ); + + const fontFaces = useMemo( () => { + return Object.values( fontFamilies ) + .flat() + .map( ( family ) => family.fontFace ) + .filter( Boolean ) + .flat(); + }, [ fontFamilies ] ); + + const loadFontFaceAsset = useCallback( + async ( fontFace, ownerDocument ) => { + if ( ! fontFace.src ) { + return; + } + + const src = getDisplaySrcFromFontFace( + fontFace.src, + currentTheme?.stylesheet_uri + ); + + if ( ! src || loadedFontUrls.has( src ) ) { + return; + } + + loadFontFaceInBrowser( fontFace, src, ownerDocument ); + setLoadedFontUrls( ( prevUrls ) => new Set( prevUrls ).add( src ) ); + }, + [ currentTheme, loadedFontUrls ] + ); + + return useCallback( + ( node ) => { + if ( ! node ) { + return; + } + + const { ownerDocument } = node; + fontFaces.forEach( ( fontFace ) => + loadFontFaceAsset( fontFace, ownerDocument ) + ); + }, + [ fontFaces, loadFontFaceAsset ] + ); +} + +export default useEditorFontsResolver; diff --git a/packages/block-editor/src/components/use-editor-fonts-resolver/utils.js b/packages/block-editor/src/components/use-editor-fonts-resolver/utils.js new file mode 100644 index 00000000000000..467eb4ef82a01a --- /dev/null +++ b/packages/block-editor/src/components/use-editor-fonts-resolver/utils.js @@ -0,0 +1,103 @@ +/* + * Format the font face name to use in the font-family property of a font face. + * + * The input can be a string with the font face name or a string with multiple font face names separated by commas. + * It removes the leading and trailing quotes from the font face name. + * + * @param {string} input - The font face name. + * @return {string} The formatted font face name. + * + * Example: + * formatFontFaceName("Open Sans") => "Open Sans" + * formatFontFaceName("'Open Sans', sans-serif") => "Open Sans" + * formatFontFaceName(", 'Open Sans', 'Helvetica Neue', sans-serif") => "Open Sans" + */ +function formatFontFaceName( input ) { + if ( ! input ) { + return ''; + } + + let output = input.trim(); + if ( output.includes( ',' ) ) { + output = output + .split( ',' ) + // Finds the first item that is not an empty string. + .find( ( item ) => item.trim() !== '' ) + .trim(); + } + // Removes leading and trailing quotes. + output = output.replace( /^["']|["']$/g, '' ); + + // Firefox needs the font name to be wrapped in double quotes meanwhile other browsers don't. + if ( window.navigator.userAgent.toLowerCase().includes( 'firefox' ) ) { + output = `"${ output }"`; + } + return output; +} + +/* + * Loads the font face from a URL and adds it to the browser. + * It also adds it to the iframe document. + */ +export async function loadFontFaceInBrowser( fontFace, source, documentRef ) { + if ( typeof source !== 'string' ) { + return; + } + const dataSource = `url(${ source })`; + const newFont = new window.FontFace( + formatFontFaceName( fontFace.fontFamily ), + dataSource, + { + style: fontFace.fontStyle, + weight: fontFace.fontWeight, + } + ); + + const loadedFace = await newFont.load(); + + // Add the font to the ref document. + documentRef.fonts.add( loadedFace ); + + // Add the font to the window document. + if ( documentRef !== window.document ) { + window.document.fonts.add( loadedFace ); + } +} + +function isUrlEncoded( url ) { + if ( typeof url !== 'string' ) { + return false; + } + return url !== decodeURIComponent( url ); +} + +/* + * Retrieves the display source from a font face src. + * + * @param {string|string[]} fontSrc - The font face src. + * @param {string} baseUrl - The base URL to resolve the src. + * @return {string|undefined} The display source or undefined if the input is invalid. + */ +export function getDisplaySrcFromFontFace( fontSrc, baseUrl ) { + if ( ! fontSrc ) { + return; + } + + let src; + if ( Array.isArray( fontSrc ) ) { + src = fontSrc[ 0 ]; + } else { + src = fontSrc; + } + + if ( ! isUrlEncoded( src ) ) { + src = encodeURI( src ); + } + + // If baseUrl is provided, use it to resolve the src. + if ( src.startsWith( 'file:.' ) ) { + src = baseUrl + '/' + src.replace( 'file:./', '' ); + } + + return src; +} diff --git a/packages/edit-site/src/components/global-styles-renderer/index.js b/packages/edit-site/src/components/global-styles-renderer/index.js index 2e840a7acdc375..0b65627a7bb443 100644 --- a/packages/edit-site/src/components/global-styles-renderer/index.js +++ b/packages/edit-site/src/components/global-styles-renderer/index.js @@ -4,6 +4,7 @@ import { useEffect } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { store as coreDataStore } from '@wordpress/core-data'; /** * Internal dependencies @@ -15,12 +16,18 @@ import { TEMPLATE_POST_TYPE } from '../../utils/constants'; const { useGlobalStylesOutput } = unlock( blockEditorPrivateApis ); function useGlobalStylesRenderer() { - const postType = useSelect( ( select ) => { - return select( editSiteStore ).getEditedPostType(); + const { postType, currentTheme } = useSelect( ( select ) => { + return { + postType: select( editSiteStore ).getEditedPostType(), + currentTheme: select( coreDataStore ).getCurrentTheme(), + }; } ); const [ styles, settings ] = useGlobalStylesOutput( postType !== TEMPLATE_POST_TYPE ); + + settings.currentTheme = currentTheme; + const { getSettings } = useSelect( editSiteStore ); const { updateSettings } = useDispatch( editSiteStore );