-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a client-side font face resolver in the editor #65019
base: trunk
Are you sure you want to change the base?
Changes from 12 commits
6219a6d
1e8ef55
65f3754
77f7638
0bed01d
48bfe65
65d147a
c6a32f9
5a9e2d0
27b5e64
463fa95
9bd3378
5cecd18
c262896
8ced871
ee98b6e
1461989
8172039
7cb4c01
20a00e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,72 @@ | ||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||
* 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: | ||||||||||||||||||||||||||||||||||||||||||
// Disable Reason: Using 'core' as string to avoid circular dependency importing from @wordpress/core-data. | ||||||||||||||||||||||||||||||||||||||||||
// eslint-disable-next-line @wordpress/data-no-store-string-literals | ||||||||||||||||||||||||||||||||||||||||||
select( 'core' )?.getCurrentTheme(), | ||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not allowed in the block-editor package. We shouldn't add an eslint exception. Why do we need the theme object here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To get the base url of the theme's fonts. Example: the theme.json data has something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to find an alternative solution. The block editor is a generic package independent of WordPress and can't make REST API calls. The alternative I suggested initially is to "resolve" the files in the server and add a new There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Do you mean when the
Sorry but I don't understand this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, basically in this place, in the code base. We read global styles and enqueue "styles" setting in the block editor. We could add a new "style" in that array with a specific I know that function is in WordPress Core but in Gutenberg we have a hook to override the output of this function I know that we used this approach in the past for "svg filters" which are also "special styles" that are not just CSS. @ajlende would know more but I think we moved a little bit from it (not entirely sure why though) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm trying to follow the pattern of this PR using the _links array. It seems to work with the regular theme.json, but I'm unable to access the links for the style variations. How I can pass the variation _settings to the editor settings? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm trying to understand the use case. What do we need to do with the resolved font paths? Preload the fonts? Could you provide some testing steps and expectations for style variations? Just to confirm, if I run
Aren't the links already available in the settings? It's private, so, in the useEditorFontsResolver I just logged the settings out in that hook and I can see the font I'm not sure that helps, maybe it leads you in the right direction? Just to clarify and contrast what I was working on with background images, the resolved background image paths are used to build the correct CSS. It's done at this point in the global styles output generation: gutenberg/packages/block-editor/src/components/global-styles/use-global-styles-output.js Lines 308 to 314 in ee98b6e
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is the issue we are trying to fix: #59965 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this commit I added the JS consumption of the _links. It seems to work for the main editor but not for the editors loaded to display the style variations. The commit includes a console.log to display the font families and the _links. gutenberg/packages/block-editor/src/components/use-editor-fonts-resolver/index.js Lines 22 to 34 in 1461989
In the screencast you can see how the editors instances created to display the style variations doesn´t get the right list of font families (the ones from the style variation) and the _links array is empty. Screencast.from.27-09-24.14.04.29.webmIs there any way to get the right list of font families and _links in the editors used to feature a style variation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So it looks like here you need all the variations' fonts at once for the sidebar. Is that right? If you look at where the key value is set in the settings, you'll notice that the That's why the resolved paths for style variations only work once you click on them, because it's it that point they're loaded into the editor. So you'll probably need to iterate over the output of However you can't use these core selectors in the block editor package, that is, in Rather, you might like to tackle from the edit-site package, where the style variations are rendered. For example, export |
||||||||||||||||||||||||||||||||||||||||||
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
Comment on lines
+21
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we describe why we need to do this? I can see what but at a casual glance I'm not sure as to why. |
||
// 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 }"`; | ||
} | ||
Comment on lines
+31
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if we just let all browsers have the wrapped version? |
||
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use
blockEditorStore
to be specific and differentiate from the editor package's store.