/** * WordPress dependencies */ import { useContext, useEffect, useState, useMemo, createInterpolateElement, } from '@wordpress/element'; import { __experimentalSpacer as Spacer, __experimentalText as WCText, __experimentalHStack as HStack, __experimentalVStack as VStack, Navigator, __experimentalHeading as Heading, Notice, SelectControl, Flex, Button, DropdownMenu, SearchControl, ProgressBar, CheckboxControl, } from '@wordpress/components'; import { debounce } from '@wordpress/compose'; import { sprintf, __, _x, isRTL } from '@wordpress/i18n'; import { moreVertical, next, previous, chevronLeft, chevronRight, } from '@wordpress/icons'; import { useEntityRecord } from '@wordpress/core-data'; import type { FontCollection as FontCollectionType, FontFace, FontFamily, CollectionFontFamily, } from '@wordpress/core-data'; /** * Internal dependencies */ import { FontLibraryContext } from './context'; import FontCard from './font-card'; import filterFonts from './utils/filter-fonts'; import { toggleFont } from './utils/toggleFont'; import { getFontsOutline, isFontFontFaceInOutline, } from './utils/fonts-outline'; import GoogleFontsConfirmDialog from './google-fonts-confirm-dialog'; import { downloadFontFaceAssets } from './utils'; import { sortFontFaces } from './utils/sort-font-faces'; import CollectionFontVariant from './collection-font-variant'; import type { FontFamilyToUpload } from './types'; const DEFAULT_CATEGORY = { slug: 'all', name: _x( 'All', 'font categories' ), }; const LOCAL_STORAGE_ITEM = 'wp-font-library-google-fonts-permission'; const MIN_WINDOW_HEIGHT = 500; function FontCollection( { slug }: { slug: string } ) { const requiresPermission = slug === 'google-fonts'; const getGoogleFontsPermissionFromStorage = () => { return window.localStorage.getItem( LOCAL_STORAGE_ITEM ) === 'true'; }; const [ selectedFont, setSelectedFont ] = useState< FontFamily | null >( null ); const [ lastSelectedFontSlug, setLastSelectedFontSlug ] = useState< string | undefined >( undefined ); const [ notice, setNotice ] = useState< { type: 'success' | 'error' | 'info'; message: string; } | null >( null ); const [ fontsToInstall, setFontsToInstall ] = useState< FontFamily[] >( [] ); const [ page, setPage ] = useState( 1 ); const [ filters, setFilters ] = useState< { category?: string; search?: string; } >( {} ); const [ renderConfirmDialog, setRenderConfirmDialog ] = useState( requiresPermission && ! getGoogleFontsPermissionFromStorage() ); const { installFonts, isInstalling } = useContext( FontLibraryContext ); const { record: selectedCollection, isResolving: isLoading } = useEntityRecord< FontCollectionType >( 'root', 'fontCollection', slug ); useEffect( () => { const handleStorage = () => { setRenderConfirmDialog( requiresPermission && ! getGoogleFontsPermissionFromStorage() ); }; handleStorage(); window.addEventListener( 'storage', handleStorage ); return () => window.removeEventListener( 'storage', handleStorage ); }, [ slug, requiresPermission ] ); const revokeAccess = () => { window.localStorage.setItem( LOCAL_STORAGE_ITEM, 'false' ); window.dispatchEvent( new Event( 'storage' ) ); }; useEffect( () => { setSelectedFont( null ); }, [ slug ] ); useEffect( () => { // If the selected fonts change, reset the selected fonts to install setFontsToInstall( [] ); }, [ selectedFont ] ); const collectionFonts = useMemo( () => ( selectedCollection?.font_families as | CollectionFontFamily[] | undefined ) ?? [], [ selectedCollection ] ); const collectionCategories = selectedCollection?.categories ?? []; const categories = [ DEFAULT_CATEGORY, ...collectionCategories ]; const fonts = useMemo( () => filterFonts( collectionFonts, filters ), [ collectionFonts, filters ] ); // NOTE: The height of the font library modal unavailable to use for rendering font family items is roughly 417px // The height of each font family item is 61px. const windowHeight = Math.max( window.innerHeight, MIN_WINDOW_HEIGHT ); const pageSize = Math.floor( ( windowHeight - 417 ) / 61 ); const totalPages = Math.ceil( fonts.length / pageSize ); const itemsStart = ( page - 1 ) * pageSize; const itemsLimit = page * pageSize; const items = fonts.slice( itemsStart, itemsLimit ); const handleCategoryFilter = ( category: string ) => { setFilters( { ...filters, category } ); setPage( 1 ); }; const handleUpdateSearchInput = ( value: string ) => { setFilters( { ...filters, search: value } ); setPage( 1 ); }; // @ts-expect-error const debouncedUpdateSearchInput = debounce( handleUpdateSearchInput, 300 ); const handleToggleVariant = ( font: FontFamily, face?: FontFace ) => { const newFontsToInstall = toggleFont( font, face, fontsToInstall ); setFontsToInstall( newFontsToInstall ); }; const fontToInstallOutline = getFontsOutline( fontsToInstall ); const resetFontsToInstall = () => { setFontsToInstall( [] ); }; const selectFontCount = fontsToInstall.length > 0 ? fontsToInstall[ 0 ]?.fontFace?.length ?? 0 : 0; // Check if any fonts are selected. const isIndeterminate = selectFontCount > 0 && selectFontCount !== selectedFont?.fontFace?.length; // Check if all fonts are selected. const isSelectAllChecked = selectFontCount === selectedFont?.fontFace?.length; // Toggle select all fonts. const toggleSelectAll = () => { const newFonts: FontFamily[] = []; if ( ! isSelectAllChecked && selectedFont ) { newFonts.push( selectedFont ); } setFontsToInstall( newFonts ); }; const handleInstall = async () => { setNotice( null ); const fontFamily: FontFamilyToUpload = fontsToInstall[ 0 ]; try { if ( fontFamily?.fontFace ) { await Promise.all( fontFamily.fontFace.map( async ( fontFace ) => { if ( fontFace.src ) { fontFace.file = await downloadFontFaceAssets( fontFace.src ); } } ) ); } } catch { // If any of the fonts fail to download, // show an error notice and stop the request from being sent. setNotice( { type: 'error', message: __( 'Error installing the fonts, could not be downloaded.' ), } ); return; } try { await installFonts( [ fontFamily ] ); setNotice( { type: 'success', message: __( 'Fonts were installed successfully.' ), } ); } catch ( error ) { setNotice( { type: 'error', message: ( error as Error ).message, } ); } resetFontsToInstall(); }; const getSortedFontFaces = ( fontFamily: FontFamily ) => { if ( ! fontFamily ) { return []; } if ( ! fontFamily.fontFace || ! fontFamily.fontFace.length ) { return [ { fontFamily: fontFamily.fontFamily, fontStyle: 'normal', fontWeight: '400', }, ]; } return sortFontFaces( fontFamily.fontFace ); }; if ( renderConfirmDialog ) { return ; } const showActions = slug === 'google-fonts' && ! renderConfirmDialog && ! selectedFont; return (
{ isLoading && (
) } { ! isLoading && selectedCollection && ( <> { selectedCollection.name } { selectedCollection.description } { showActions && ( ) } { categories && categories.map( ( category ) => ( ) ) } { !! selectedCollection?.font_families?.length && ! fonts.length && ( { __( 'No fonts found. Try with a different search term.' ) } ) }
{ /* * Disable reason: The `list` ARIA role is redundant but * Safari+VoiceOver won't announce the list otherwise. */ /* eslint-disable jsx-a11y/no-redundant-roles */ }
    { items.map( ( font ) => (
  • { setSelectedFont( font.font_family_settings ); } } />
  • ) ) }
{ /* eslint-enable jsx-a11y/no-redundant-roles */ }
{ setLastSelectedFontSlug( selectedFont?.slug ); setSelectedFont( null ); setNotice( null ); } } label={ __( 'Back' ) } /> { selectedFont?.name } { notice && ( <> setNotice( null ) } > { notice.message } ) } { __( 'Select font variants to install.' ) } { /* * Disable reason: The `list` ARIA role is redundant but * Safari+VoiceOver won't announce the list otherwise. */ /* eslint-disable jsx-a11y/no-redundant-roles */ }
    { selectedFont && getSortedFontFaces( selectedFont ).map( ( face, i ) => (
  • ) ) }
{ /* eslint-enable jsx-a11y/no-redundant-roles */ }
{ selectedFont && ( ) } { ! selectedFont && ( { createInterpolateElement( sprintf( // translators: 1: Current page number, 2: Total number of pages. _x( '
Page
%1$s
of %2$d
', 'paging' ), '', totalPages ), { div:
, // @ts-expect-error — Tag injected via sprintf argument, not visible in format string. CurrentPage: ( { return { label: ( i + 1 ).toString(), value: ( i + 1 ).toString(), }; } ) } onChange={ ( newPage ) => setPage( parseInt( newPage ) ) } size="small" variant="minimal" /> ), } ) }
); } export default FontCollection;