/** * WordPress dependencies */ import { Button, __experimentalConfirmDialog as ConfirmDialog, __experimentalHStack as HStack, __experimentalHeading as Heading, Navigator, useNavigator, __experimentalSpacer as Spacer, __experimentalText as WCText, __experimentalVStack as VStack, Flex, Notice, ProgressBar, CheckboxControl, } from '@wordpress/components'; import { useEntityRecord, store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; import { useContext, useEffect, useMemo, useState } from '@wordpress/element'; import { __, _x, sprintf, isRTL } from '@wordpress/i18n'; import { chevronLeft, chevronRight } from '@wordpress/icons'; import type { FontFamilyPreset, GlobalStylesConfig, } from '@wordpress/global-styles-engine'; import type { FontFamily } from '@wordpress/core-data'; /** * Internal dependencies */ import { FontLibraryContext } from './context'; import FontCard from './font-card'; import LibraryFontVariant from './library-font-variant'; import { sortFontFaces } from './utils/sort-font-faces'; import { setUIValuesNeeded, loadFontFaceInBrowser, unloadFontFaceInBrowser, getDisplaySrcFromFontFace, } from './utils'; import { useSetting } from '../hooks'; /** * Comparison key for font families. Sorts families by slug and faces * by style+weight so order differences don't produce false positives. * * @param fontFamilies Font families record keyed by source (e.g. "theme", "custom"). */ function getFontFamiliesKey( fontFamilies: Record< string, FontFamilyPreset[] > | undefined ): string { if ( ! fontFamilies ) { return ''; } const normalized: Record< string, unknown[] > = {}; for ( const source of Object.keys( fontFamilies ).sort() ) { normalized[ source ] = ( fontFamilies[ source ] ?? [] ) .map( ( family ) => ( { slug: family.slug, fontFace: ( family.fontFace ?? [] ) .map( ( face ) => `${ face.fontStyle }-${ face.fontWeight }` ) .sort(), } ) ) .sort( ( a, b ) => a.slug.localeCompare( b.slug ) ); } return JSON.stringify( normalized ); } function InstalledFonts() { const { baseCustomFonts, libraryFontSelected, handleSetLibraryFontSelected, uninstallFontFamily, isResolvingLibrary, isInstalling, saveFontFamilies, getFontFacesActivated, } = useContext( FontLibraryContext ); const [ fontFamilies, setFontFamilies ] = useSetting< Record< string, FontFamilyPreset[] > | undefined >( 'typography.fontFamilies' ); const [ lastSelectedFontSlug, setLastSelectedFontSlug ] = useState< string | undefined >( undefined ); const [ isConfirmDeleteOpen, setIsConfirmDeleteOpen ] = useState< boolean >( false ); const [ notice, setNotice ] = useState< { type: 'success' | 'error' | 'info'; message: string; } | null >( null ); const [ baseFontFamilies ] = useSetting< Record< string, FontFamilyPreset[] > | undefined >( 'typography.fontFamilies', undefined, 'base' ); const globalStylesId = useSelect( ( select ) => { const { __experimentalGetCurrentGlobalStylesId } = select( coreStore ); return __experimentalGetCurrentGlobalStylesId(); }, [] ); const globalStyles = useEntityRecord< GlobalStylesConfig >( 'root', 'globalStyles', globalStylesId ); const editedFontFamilies = globalStyles?.edits?.settings?.typography?.fontFamilies; const savedFontFamilies = globalStyles?.record?.settings?.typography?.fontFamilies; const fontFamiliesHasChanges = useMemo( () => { if ( editedFontFamilies === undefined ) { return false; } return ( getFontFamiliesKey( editedFontFamilies ) !== getFontFamiliesKey( savedFontFamilies ) ); }, [ editedFontFamilies, savedFontFamilies ] ); const themeFonts = fontFamilies?.theme ? fontFamilies.theme .map( ( f ) => setUIValuesNeeded( f, { source: 'theme' } ) ) .sort( ( a, b ) => a.name.localeCompare( b.name ) ) : []; const themeFontsSlugs = new Set( themeFonts.map( ( f ) => f.slug ) ); const baseThemeFonts = baseFontFamilies?.theme ? themeFonts.concat( baseFontFamilies.theme .filter( ( f ) => ! themeFontsSlugs.has( f.slug ) ) .map( ( f ) => setUIValuesNeeded( f, { source: 'theme' } ) ) .sort( ( a, b ) => a.name.localeCompare( b.name ) ) ) : []; const customFontFamilyId = libraryFontSelected?.source === 'custom' && libraryFontSelected?.id; const canUserDelete = useSelect( ( select ) => { const { canUser } = select( coreStore ); return ( customFontFamilyId && canUser( 'delete', { kind: 'postType', name: 'wp_font_family', id: customFontFamilyId, } ) ); }, [ customFontFamilyId ] ); const shouldDisplayDeleteButton = !! libraryFontSelected && libraryFontSelected?.source !== 'theme' && canUserDelete; const handleUninstallClick = () => { setIsConfirmDeleteOpen( true ); }; const handleUpdate = async () => { setNotice( null ); try { await saveFontFamilies( fontFamilies ); setNotice( { type: 'success', message: __( 'Font family updated successfully.' ), } ); } catch ( error ) { setNotice( { type: 'error', message: sprintf( /* translators: %s: error message */ __( 'There was an error updating the font family. %s' ), ( error as Error ).message ), } ); } }; const getFontFacesToDisplay = ( font: FontFamily ) => { if ( ! font ) { return []; } if ( ! font.fontFace || ! font.fontFace.length ) { return [ { fontFamily: font.fontFamily, fontStyle: 'normal', fontWeight: '400', }, ]; } return sortFontFaces( font.fontFace ); }; const getFontCardVariantsText = ( font: FontFamily ) => { const variantsInstalled = font?.fontFace && ( font?.fontFace?.length ?? 0 ) > 0 ? font.fontFace.length : 1; const variantsActive = getFontFacesActivated( font.slug, font.source ).length; return sprintf( /* translators: 1: Active font variants, 2: Total font variants. */ __( '%1$d of %2$d active' ), variantsActive, variantsInstalled ); }; useEffect( () => { handleSetLibraryFontSelected( libraryFontSelected ); }, [] ); // Get activated fonts count. const activeFontsCount = libraryFontSelected ? getFontFacesActivated( libraryFontSelected.slug, libraryFontSelected.source ).length : 0; const selectedFontsCount = libraryFontSelected?.fontFace?.length ?? ( libraryFontSelected?.fontFamily ? 1 : 0 ); // Check if any fonts are selected. const isIndeterminate = activeFontsCount > 0 && activeFontsCount !== selectedFontsCount; // Check if all fonts are selected. const isSelectAllChecked = activeFontsCount === selectedFontsCount; // Toggle select all fonts. const toggleSelectAll = () => { if ( ! libraryFontSelected || ! libraryFontSelected?.source ) { return; } const initialFonts = fontFamilies?.[ libraryFontSelected.source ]?.filter( ( f ) => f.slug !== libraryFontSelected.slug ) ?? []; const newFonts = isSelectAllChecked ? initialFonts : [ ...initialFonts, libraryFontSelected ]; setFontFamilies( { ...fontFamilies, [ libraryFontSelected.source ]: newFonts, } ); if ( libraryFontSelected.fontFace ) { libraryFontSelected.fontFace.forEach( ( face ) => { if ( isSelectAllChecked ) { unloadFontFaceInBrowser( face, 'all' ); } else { const displaySrc = getDisplaySrcFromFontFace( face?.src ?? '' ); if ( displaySrc ) { loadFontFaceInBrowser( face, displaySrc, 'all' ); } } } ); } }; const hasFonts = baseThemeFonts.length > 0 || baseCustomFonts.length > 0; return (