diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index 864d703f260b4..2b9efd2ddccd6 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -55,11 +55,21 @@ function FontLibraryProvider( { children } ) { const [ isInstalling, setIsInstalling ] = useState( false ); const [ refreshKey, setRefreshKey ] = useState( 0 ); + const [ notice, setNotice ] = useState( null ); const refreshLibrary = () => { setRefreshKey( Date.now() ); }; + // Reset notice on dismiss. + useEffect( () => { + if ( notice ) { + notice.onRemove = () => { + setNotice( null ); + }; + } + }, [ notice, setNotice ] ); + const { records: libraryPosts = [], isResolving: isResolvingLibrary, @@ -134,6 +144,8 @@ function FontLibraryProvider( { children } ) { }, [ modalTabOpen ] ); const handleSetLibraryFontSelected = ( font ) => { + setNotice( null ); + // If font is null, reset the selected font if ( ! font ) { setLibraryFontSelected( null ); @@ -471,6 +483,8 @@ function FontLibraryProvider( { children } ) { modalTabOpen, toggleModal, refreshLibrary, + notice, + setNotice, saveFontFamilies, fontFamiliesHasChanges, isResolvingLibrary, diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index 46363365363bb..3d2a0d9b2cdfa 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -12,7 +12,6 @@ import { FlexItem, Flex, Button, - Notice, } from '@wordpress/components'; import { debounce } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; @@ -47,14 +46,13 @@ function FontCollection( { slug } ) { ); }; - const [ notice, setNotice ] = useState( null ); const [ selectedFont, setSelectedFont ] = useState( null ); const [ fontsToInstall, setFontsToInstall ] = useState( [] ); const [ filters, setFilters ] = useState( {} ); const [ renderConfirmDialog, setRenderConfirmDialog ] = useState( requiresPermission && ! getGoogleFontsPermissionFromStorage() ); - const { collections, getFontCollection, installFont } = + const { collections, getFontCollection, installFont, notice, setNotice } = useContext( FontLibraryContext ); const selectedCollection = collections.find( ( collection ) => collection.slug === slug @@ -77,36 +75,27 @@ function FontCollection( { slug } ) { await getFontCollection( slug ); resetFilters(); } catch ( e ) { - setNotice( { - type: 'error', - message: e?.message, - duration: 0, // Don't auto-hide. - } ); + if ( ! notice ) { + setNotice( { + type: 'error', + message: e?.message, + } ); + } } }; fetchFontCollection(); - }, [ slug, getFontCollection ] ); + }, [ slug, getFontCollection, setNotice, notice ] ); useEffect( () => { setSelectedFont( null ); setNotice( null ); - }, [ slug ] ); + }, [ slug, setNotice ] ); useEffect( () => { // If the selected fonts change, reset the selected fonts to install setFontsToInstall( [] ); }, [ selectedFont ] ); - // Reset notice after 5 seconds - useEffect( () => { - if ( notice && notice?.duration !== 0 ) { - const timeout = setTimeout( () => { - setNotice( null ); - }, notice.duration ?? 5000 ); - return () => clearTimeout( timeout ); - } - }, [ notice ] ); - const collectionFonts = useMemo( () => selectedCollection?.font_families ?? [], [ selectedCollection ] @@ -154,6 +143,8 @@ function FontCollection( { slug } ) { }; const handleInstall = async () => { + setNotice( null ); + const fontFamily = fontsToInstall[ 0 ]; try { @@ -205,6 +196,7 @@ function FontCollection( { slug } ) { ? selectedCollection.description : __( 'Select font variants to install.' ) } + notice={ notice } handleBack={ !! selectedFont && handleUnselectFont } footer={ fontsToInstall.length > 0 && ( @@ -219,22 +211,6 @@ function FontCollection( { slug } ) { ) } - { notice && ( - <> - - - - { notice.message } - - - - - ) } - { ! renderConfirmDialog && ! selectedFont && ( diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-demo.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-demo.js index 0c464ac271580..9ed025dcc17b4 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-demo.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-demo.js @@ -62,7 +62,7 @@ function FontFaceDemo( { customPreviewUrl, fontFace, text, style = {} } ) { } }; loadAsset(); - }, [ fontFace, isIntersecting, loadFontFaceAsset ] ); + }, [ fontFace, isIntersecting, loadFontFaceAsset, isPreviewImage ] ); return (
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/index.js index a68c42ec01041..c2c2d05e0c15b 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/index.js @@ -43,13 +43,18 @@ function FontLibraryModal( { onRequestClose, initialTabId = 'installed-fonts', } ) { - const { collections } = useContext( FontLibraryContext ); + const { collections, setNotice } = useContext( FontLibraryContext ); const tabs = [ ...DEFAULT_TABS, ...tabsFromCollections( collections || [] ), ]; + // Reset notice when new tab is selected. + const onSelect = () => { + setNotice( null ); + }; + return (
- + { tabs.map( ( { id, title } ) => ( diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js index 3a13a720dc7c9..ec372f3b1fd13 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js @@ -9,7 +9,6 @@ import { __experimentalSpacer as Spacer, Button, Spinner, - Notice, FlexItem, } from '@wordpress/components'; @@ -34,6 +33,8 @@ function InstalledFonts() { refreshLibrary, uninstallFontFamily, isResolvingLibrary, + notice, + setNotice, } = useContext( FontLibraryContext ); const [ isConfirmDeleteOpen, setIsConfirmDeleteOpen ] = useState( false ); @@ -45,9 +46,9 @@ function InstalledFonts() { handleSetLibraryFontSelected( font ); }; - const [ notice, setNotice ] = useState( null ); - const handleConfirmUninstall = async () => { + setNotice( null ); + try { await uninstallFontFamily( libraryFontSelected ); setNotice( { @@ -91,20 +92,11 @@ function InstalledFonts() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [] ); - // Reset notice after 5 seconds - useEffect( () => { - if ( notice ) { - const timeout = setTimeout( () => { - setNotice( null ); - }, 5000 ); - return () => clearTimeout( timeout ); - } - }, [ notice ] ); - return ( - { notice && ( - <> - - - - { notice.message } - - - - - ) } - { ! libraryFontSelected && ( <> - { isResolvingLibrary && } + { isResolvingLibrary && ( + + + + + + ) } { baseCustomFonts.length > 0 && ( <> - { baseCustomFonts.map( ( font ) => ( `.${ extension }` ) - .join( ', ' ) + - ` ${ __( 'and' ) } .${ ALLOWED_FILE_EXTENSIONS.slice( -1 ) }`; - - const handleDropZone = ( files ) => { - handleFilesUpload( files ); - }; - const onFilesUpload = ( event ) => { - handleFilesUpload( event.target.files ); - }; - - // Reset notice after 5 seconds - useEffect( () => { - if ( notice ) { - const timeout = setTimeout( () => { - setNotice( null ); - }, 5000 ); - return () => clearTimeout( timeout ); - } - }, [ notice ] ); - - /** - * Filters the selected files to only allow the ones with the allowed extensions - * - * @param {Array} files The files to be filtered - * @return {void} - */ - const handleFilesUpload = ( files ) => { - setNotice( null ); - setIsUploading( true ); - const uniqueFilenames = new Set(); - const selectedFiles = [ ...files ]; - const allowedFiles = selectedFiles.filter( ( file ) => { - if ( uniqueFilenames.has( file.name ) ) { - return false; // Discard duplicates - } - // Eliminates files that are not allowed - const fileExtension = file.name.split( '.' ).pop().toLowerCase(); - if ( ALLOWED_FILE_EXTENSIONS.includes( fileExtension ) ) { - uniqueFilenames.add( file.name ); - return true; // Keep file if the extension is allowed - } - return false; // Discard file extension not allowed - } ); - if ( allowedFiles.length > 0 ) { - loadFiles( allowedFiles ); - } - }; - - /** - * Loads the selected files and reads the font metadata - * - * @param {Array} files The files to be loaded - * @return {void} - */ - const loadFiles = async ( files ) => { - const fontFacesLoaded = await Promise.all( - files.map( async ( fontFile ) => { - const fontFaceData = await getFontFaceMetadata( fontFile ); - await loadFontFaceInBrowser( - fontFaceData, - fontFaceData.file, - 'all' - ); - return fontFaceData; - } ) - ); - await handleInstall( fontFacesLoaded ); - }; - - // Create a function to read the file as array buffer - async function readFileAsArrayBuffer( file ) { - return new Promise( ( resolve, reject ) => { - const reader = new window.FileReader(); - reader.readAsArrayBuffer( file ); - reader.onload = () => resolve( reader.result ); - reader.onerror = reject; - } ); - } - - const getFontFaceMetadata = async ( fontFile ) => { - const buffer = await readFileAsArrayBuffer( fontFile ); - const fontObj = new Font( 'Uploaded Font' ); - fontObj.fromDataBuffer( buffer, fontFile.name ); - // Assuming that fromDataBuffer triggers onload event and returning a Promise - const onloadEvent = await new Promise( - ( resolve ) => ( fontObj.onload = resolve ) - ); - const font = onloadEvent.detail.font; - const { name } = font.opentype.tables; - const fontName = name.get( 16 ) || name.get( 1 ); - const isItalic = name.get( 2 ).toLowerCase().includes( 'italic' ); - const fontWeight = - font.opentype.tables[ 'OS/2' ].usWeightClass || 'normal'; - const isVariable = !! font.opentype.tables.fvar; - const weightAxis = - isVariable && - font.opentype.tables.fvar.axes.find( - ( { tag } ) => tag === 'wght' - ); - const weightRange = weightAxis - ? `${ weightAxis.minValue } ${ weightAxis.maxValue }` - : null; - return { - file: fontFile, - fontFamily: fontName, - fontStyle: isItalic ? 'italic' : 'normal', - fontWeight: weightRange || fontWeight, - }; - }; - - /** - * Creates the font family definition and sends it to the server - * - * @param {Array} fontFaces The font faces to be installed - * @return {void} - */ - const handleInstall = async ( fontFaces ) => { - const fontFamilies = makeFamiliesFromFaces( fontFaces ); - - if ( fontFamilies.length > 1 ) { - setNotice( { - type: 'error', - message: __( - 'Variants from only one font family can be uploaded at a time.' - ), - } ); - setIsUploading( false ); - return; - } - - try { - await installFont( fontFamilies[ 0 ] ); - setNotice( { - type: 'success', - message: __( 'Fonts were installed successfully.' ), - } ); - } catch ( error ) { - setNotice( { - type: 'error', - message: error.message, - } ); - } - - setIsUploading( false ); - }; - - return ( - <> - - - - { ! isUploading && ( - `.${ ext }` - ).join( ',' ) } - multiple={ true } - onChange={ onFilesUpload } - render={ ( { openFileDialog } ) => ( - - ) } - /> - ) } - { isUploading && ( - -
- -
-
- ) } - - - { sprintf( - /* translators: %s: supported font formats: ex: .ttf, .woff and .woff2 */ - __( - 'Uploaded fonts appear in your library and can be used in your theme. Supported formats: %s.' - ), - supportedFormats - ) } - - { ! isUploading && notice && ( - - - - { notice.message } - - - ) } -
- - ); -} - -export default LocalFonts; diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss index 5d199e9ce680e..d8020b3b672e0 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss +++ b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss @@ -25,11 +25,6 @@ } .font-library-modal__tabpanel-layout { - - .font-library-modal__tabpanel-layout__main { - padding-bottom: $grid-unit-80; - } - .font-library-modal__tabpanel-layout__footer { border-top: 1px solid $gray-300; margin: 0 #{$grid-unit-40 * -1} #{$grid-unit-40 * -1}; @@ -39,7 +34,6 @@ width: 100%; background-color: $white; } - } .font-library-modal__fonts-grid { @@ -107,10 +101,6 @@ button.font-library-modal__upload-area { .font-library-modal__upload-area__text { color: $gray-700; } - - .font-library-modal__upload-area__notice { - margin: 0; - } } .font-library-modal__font-variant_demo-wrapper { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/tab-panel-layout.js b/packages/edit-site/src/components/global-styles/font-library-modal/tab-panel-layout.js index d2adb542af2a6..13c2d04db7914 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/tab-panel-layout.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/tab-panel-layout.js @@ -8,12 +8,15 @@ import { __experimentalSpacer as Spacer, __experimentalHStack as HStack, Button, + Notice, + FlexBlock, } from '@wordpress/components'; import { chevronLeft } from '@wordpress/icons'; function TabPanelLayout( { title, description, + notice, handleBack, children, footer, @@ -43,6 +46,18 @@ function TabPanelLayout( { ) } { description && { description } } + { notice && ( + + + + { notice.message } + + + + ) }
{ children } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/upload-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/upload-fonts.js index effc4a2a5227c..f183ff962c778 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/upload-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/upload-fonts.js @@ -1,19 +1,214 @@ /** * WordPress dependencies */ -import { __experimentalSpacer as Spacer } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { + Button, + DropZone, + __experimentalSpacer as Spacer, + __experimentalText as Text, + __experimentalVStack as VStack, + FormFileUpload, + FlexItem, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; +import { useContext, useState } from '@wordpress/element'; /** * Internal dependencies */ -import LocalFonts from './local-fonts'; +import { ALLOWED_FILE_EXTENSIONS } from './utils/constants'; +import { FontLibraryContext } from './context'; +import { Font } from '../../../../lib/lib-font.browser'; +import makeFamiliesFromFaces from './utils/make-families-from-faces'; +import { loadFontFaceInBrowser } from './utils'; +import TabPanelLayout from './tab-panel-layout'; +import { unlock } from '../../../lock-unlock'; + +const { ProgressBar } = unlock( componentsPrivateApis ); function UploadFonts() { + const { installFont, notice, setNotice } = useContext( FontLibraryContext ); + const [ isUploading, setIsUploading ] = useState( false ); + const supportedFormats = + ALLOWED_FILE_EXTENSIONS.slice( 0, -1 ) + .map( ( extension ) => `.${ extension }` ) + .join( ', ' ) + + ` ${ __( 'and' ) } .${ ALLOWED_FILE_EXTENSIONS.slice( -1 ) }`; + + const handleDropZone = ( files ) => { + handleFilesUpload( files ); + }; + const onFilesUpload = ( event ) => { + handleFilesUpload( event.target.files ); + }; + + /** + * Filters the selected files to only allow the ones with the allowed extensions + * + * @param {Array} files The files to be filtered + * @return {void} + */ + const handleFilesUpload = ( files ) => { + setNotice( null ); + setIsUploading( true ); + const uniqueFilenames = new Set(); + const selectedFiles = [ ...files ]; + const allowedFiles = selectedFiles.filter( ( file ) => { + if ( uniqueFilenames.has( file.name ) ) { + return false; // Discard duplicates + } + // Eliminates files that are not allowed + const fileExtension = file.name.split( '.' ).pop().toLowerCase(); + if ( ALLOWED_FILE_EXTENSIONS.includes( fileExtension ) ) { + uniqueFilenames.add( file.name ); + return true; // Keep file if the extension is allowed + } + return false; // Discard file extension not allowed + } ); + if ( allowedFiles.length > 0 ) { + loadFiles( allowedFiles ); + } + }; + + /** + * Loads the selected files and reads the font metadata + * + * @param {Array} files The files to be loaded + * @return {void} + */ + const loadFiles = async ( files ) => { + const fontFacesLoaded = await Promise.all( + files.map( async ( fontFile ) => { + const fontFaceData = await getFontFaceMetadata( fontFile ); + await loadFontFaceInBrowser( + fontFaceData, + fontFaceData.file, + 'all' + ); + return fontFaceData; + } ) + ); + handleInstall( fontFacesLoaded ); + }; + + // Create a function to read the file as array buffer + async function readFileAsArrayBuffer( file ) { + return new Promise( ( resolve, reject ) => { + const reader = new window.FileReader(); + reader.readAsArrayBuffer( file ); + reader.onload = () => resolve( reader.result ); + reader.onerror = reject; + } ); + } + + const getFontFaceMetadata = async ( fontFile ) => { + const buffer = await readFileAsArrayBuffer( fontFile ); + const fontObj = new Font( 'Uploaded Font' ); + fontObj.fromDataBuffer( buffer, fontFile.name ); + // Assuming that fromDataBuffer triggers onload event and returning a Promise + const onloadEvent = await new Promise( + ( resolve ) => ( fontObj.onload = resolve ) + ); + const font = onloadEvent.detail.font; + const { name } = font.opentype.tables; + const fontName = name.get( 16 ) || name.get( 1 ); + const isItalic = name.get( 2 ).toLowerCase().includes( 'italic' ); + const fontWeight = + font.opentype.tables[ 'OS/2' ].usWeightClass || 'normal'; + const isVariable = !! font.opentype.tables.fvar; + const weightAxis = + isVariable && + font.opentype.tables.fvar.axes.find( + ( { tag } ) => tag === 'wght' + ); + const weightRange = weightAxis + ? `${ weightAxis.minValue } ${ weightAxis.maxValue }` + : null; + return { + file: fontFile, + fontFamily: fontName, + fontStyle: isItalic ? 'italic' : 'normal', + fontWeight: weightRange || fontWeight, + }; + }; + + /** + * Creates the font family definition and sends it to the server + * + * @param {Array} fontFaces The font faces to be installed + * @return {void} + */ + const handleInstall = async ( fontFaces ) => { + const fontFamilies = makeFamiliesFromFaces( fontFaces ); + + if ( fontFamilies.length > 1 ) { + setNotice( { + type: 'error', + message: __( + 'Variants from only one font family can be uploaded at a time.' + ), + } ); + setIsUploading( false ); + return; + } + + try { + await installFont( fontFamilies[ 0 ] ); + setNotice( { + type: 'success', + message: __( 'Fonts were installed successfully.' ), + } ); + } catch ( error ) { + setNotice( { + type: 'error', + message: error.message, + } ); + } + + setIsUploading( false ); + }; + return ( - <> - - - + + + + { isUploading && ( + +
+ +
+
+ ) } + { ! isUploading && ( + `.${ ext }` + ).join( ',' ) } + multiple={ true } + onChange={ onFilesUpload } + render={ ( { openFileDialog } ) => ( + + ) } + /> + ) } + + + { sprintf( + /* translators: %s: supported font formats: ex: .ttf, .woff and .woff2 */ + __( + 'Uploaded fonts appear in your library and can be used in your theme. Supported formats: %s.' + ), + supportedFormats + ) } + +
+
); }