-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* [Inserter]: Add media tab * fix mobile styles * fix media library modal * change media tab order * add tooltip, style tweaks and cleanup * memo preview blocks * clone media block before insertion * use only css for `open media library` positioning * use raw caption * inject styles to the block preview iframe content * remove `BlockPreview` usage * fix media library in safari * fix focus outside when media library button has focus * Update packages/block-editor/src/components/inserter/media-tab/media-tab.js Co-authored-by: Matias Ventura <[email protected]> * preload initial media requests * remove AsyncList and css fix * Apply suggestions from code review Co-authored-by: Miguel Fonseca <[email protected]> * add e2e test and small tweaks Co-authored-by: Matias Ventura <[email protected]> Co-authored-by: Miguel Fonseca <[email protected]>
- Loading branch information
1 parent
92aa6c8
commit d40cc5c
Showing
17 changed files
with
811 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
/** | ||
* Patches resources loaded by the block editor page. | ||
* | ||
* @package gutenberg | ||
*/ | ||
|
||
/** | ||
* Adds the preload paths registered in Core (`edit-form-blocks.php`). | ||
* | ||
* @param array $preload_paths Preload paths to be filtered. | ||
* @param WP_Block_Editor_Context $context The current block editor context. | ||
* @return array | ||
*/ | ||
// @codingStandardsIgnoreStart - unused $context parameter. | ||
function gutenberg_preload_paths_6_2( $preload_paths, $context ) { | ||
// Preload initial media requests that are needed to conditionally display the media tab in the inserter. | ||
foreach ( array( 'image', 'video', 'audio' ) as $media_type ) { | ||
$preload_paths[] = "wp/v2/media?context=view&per_page=1&_fields=id&media_type={$media_type}"; | ||
} | ||
|
||
return $preload_paths; | ||
} | ||
add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_preload_paths_6_2', 10, 2 ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
packages/block-editor/src/components/inserter/media-tab/hooks.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __ } from '@wordpress/i18n'; | ||
import { useEffect, useState } from '@wordpress/element'; | ||
import { useSelect } from '@wordpress/data'; | ||
import { useDebounce } from '@wordpress/compose'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { store as blockEditorStore } from '../../../store'; | ||
|
||
export function useDebouncedInput() { | ||
const [ input, setInput ] = useState( '' ); | ||
const [ debounced, setter ] = useState( '' ); | ||
const setDebounced = useDebounce( setter, 250 ); | ||
useEffect( () => { | ||
if ( debounced !== input ) { | ||
setDebounced( input ); | ||
} | ||
}, [ debounced, input ] ); | ||
return [ input, setInput, debounced ]; | ||
} | ||
|
||
export function useMediaResults( options = {} ) { | ||
const [ results, setResults ] = useState(); | ||
const settings = useSelect( | ||
( select ) => select( blockEditorStore ).getSettings(), | ||
[] | ||
); | ||
useEffect( () => { | ||
( async () => { | ||
setResults(); | ||
const _media = await settings?.__unstableFetchMedia( options ); | ||
if ( _media ) setResults( _media ); | ||
} )(); | ||
}, Object.values( options ) ); | ||
return results; | ||
} | ||
|
||
const MEDIA_CATEGORIES = [ | ||
{ label: __( 'Images' ), name: 'images', mediaType: 'image' }, | ||
{ label: __( 'Videos' ), name: 'videos', mediaType: 'video' }, | ||
{ label: __( 'Audio' ), name: 'audio', mediaType: 'audio' }, | ||
]; | ||
export function useMediaCategories( rootClientId ) { | ||
const [ categories, setCategories ] = useState( [] ); | ||
const { canInsertImage, canInsertVideo, canInsertAudio, fetchMedia } = | ||
useSelect( | ||
( select ) => { | ||
const { canInsertBlockType, getSettings } = | ||
select( blockEditorStore ); | ||
return { | ||
fetchMedia: getSettings().__unstableFetchMedia, | ||
canInsertImage: canInsertBlockType( | ||
'core/image', | ||
rootClientId | ||
), | ||
canInsertVideo: canInsertBlockType( | ||
'core/video', | ||
rootClientId | ||
), | ||
canInsertAudio: canInsertBlockType( | ||
'core/audio', | ||
rootClientId | ||
), | ||
}; | ||
}, | ||
[ rootClientId ] | ||
); | ||
useEffect( () => { | ||
( async () => { | ||
// If `__unstableFetchMedia` is not defined in block | ||
// editor settings, do not set any media categories. | ||
if ( ! fetchMedia ) return; | ||
const query = { | ||
context: 'view', | ||
per_page: 1, | ||
_fields: [ 'id' ], | ||
}; | ||
const [ image, video, audio ] = await Promise.all( [ | ||
fetchMedia( { ...query, media_type: 'image' } ), | ||
fetchMedia( { ...query, media_type: 'video' } ), | ||
fetchMedia( { ...query, media_type: 'audio' } ), | ||
] ); | ||
const showImage = canInsertImage && !! image.length; | ||
const showVideo = canInsertVideo && !! video.length; | ||
const showAudio = canInsertAudio && !! audio.length; | ||
setCategories( | ||
MEDIA_CATEGORIES.filter( | ||
( { mediaType } ) => | ||
( mediaType === 'image' && showImage ) || | ||
( mediaType === 'video' && showVideo ) || | ||
( mediaType === 'audio' && showAudio ) | ||
) | ||
); | ||
} )(); | ||
}, [ canInsertImage, canInsertVideo, canInsertAudio, fetchMedia ] ); | ||
return categories; | ||
} |
3 changes: 3 additions & 0 deletions
3
packages/block-editor/src/components/inserter/media-tab/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { default as MediaTab } from './media-tab'; | ||
export { MediaCategoryDialog } from './media-panel'; | ||
export { useMediaCategories } from './hooks'; |
93 changes: 93 additions & 0 deletions
93
packages/block-editor/src/components/inserter/media-tab/media-list.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { | ||
__unstableComposite as Composite, | ||
__unstableUseCompositeState as useCompositeState, | ||
__unstableCompositeItem as CompositeItem, | ||
Tooltip, | ||
} from '@wordpress/components'; | ||
import { __ } from '@wordpress/i18n'; | ||
import { useMemo, useCallback } from '@wordpress/element'; | ||
import { cloneBlock } from '@wordpress/blocks'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import InserterDraggableBlocks from '../../inserter-draggable-blocks'; | ||
import { getBlockAndPreviewFromMedia } from './utils'; | ||
|
||
function MediaPreview( { media, onClick, composite, mediaType } ) { | ||
const [ block, preview ] = useMemo( | ||
() => getBlockAndPreviewFromMedia( media, mediaType ), | ||
[ media, mediaType ] | ||
); | ||
const title = media.title?.rendered || media.title; | ||
const baseCssClass = 'block-editor-inserter__media-list'; | ||
return ( | ||
<InserterDraggableBlocks isEnabled={ true } blocks={ [ block ] }> | ||
{ ( { draggable, onDragStart, onDragEnd } ) => ( | ||
<div | ||
className={ `${ baseCssClass }__list-item` } | ||
draggable={ draggable } | ||
onDragStart={ onDragStart } | ||
onDragEnd={ onDragEnd } | ||
> | ||
<Tooltip text={ title }> | ||
<CompositeItem | ||
role="option" | ||
as="div" | ||
{ ...composite } | ||
className={ `${ baseCssClass }__item` } | ||
onClick={ () => { | ||
onClick( block ); | ||
} } | ||
aria-label={ title } | ||
> | ||
<div | ||
className={ `${ baseCssClass }__item-preview` } | ||
> | ||
{ preview } | ||
</div> | ||
</CompositeItem> | ||
</Tooltip> | ||
</div> | ||
) } | ||
</InserterDraggableBlocks> | ||
); | ||
} | ||
|
||
function MediaList( { | ||
mediaList, | ||
mediaType, | ||
onClick, | ||
label = __( 'Media List' ), | ||
} ) { | ||
const composite = useCompositeState(); | ||
const onPreviewClick = useCallback( | ||
( block ) => { | ||
onClick( cloneBlock( block ) ); | ||
}, | ||
[ onClick ] | ||
); | ||
return ( | ||
<Composite | ||
{ ...composite } | ||
role="listbox" | ||
className="block-editor-inserter__media-list" | ||
aria-label={ label } | ||
> | ||
{ mediaList.map( ( media ) => ( | ||
<MediaPreview | ||
key={ media.id } | ||
media={ media } | ||
mediaType={ mediaType } | ||
onClick={ onPreviewClick } | ||
composite={ composite } | ||
/> | ||
) ) } | ||
</Composite> | ||
); | ||
} | ||
|
||
export default MediaList; |
82 changes: 82 additions & 0 deletions
82
packages/block-editor/src/components/inserter/media-tab/media-panel.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useRef, useEffect } from '@wordpress/element'; | ||
import { Spinner, SearchControl } from '@wordpress/components'; | ||
import { focus } from '@wordpress/dom'; | ||
import { __, sprintf } from '@wordpress/i18n'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import MediaList from './media-list'; | ||
import { useMediaResults, useDebouncedInput } from './hooks'; | ||
import InserterNoResults from '../no-results'; | ||
|
||
const INITIAL_MEDIA_ITEMS_PER_PAGE = 10; | ||
|
||
export function MediaCategoryDialog( { rootClientId, onInsert, category } ) { | ||
const container = useRef(); | ||
useEffect( () => { | ||
const timeout = setTimeout( () => { | ||
const [ firstTabbable ] = focus.tabbable.find( container.current ); | ||
firstTabbable?.focus(); | ||
} ); | ||
return () => clearTimeout( timeout ); | ||
}, [ category ] ); | ||
return ( | ||
<div ref={ container } className="block-editor-inserter__media-dialog"> | ||
<MediaCategoryPanel | ||
rootClientId={ rootClientId } | ||
onInsert={ onInsert } | ||
category={ category } | ||
/> | ||
</div> | ||
); | ||
} | ||
|
||
export function MediaCategoryPanel( { rootClientId, onInsert, category } ) { | ||
const [ search, setSearch, debouncedSearch ] = useDebouncedInput(); | ||
const mediaList = useMediaResults( { | ||
per_page: !! debouncedSearch ? 20 : INITIAL_MEDIA_ITEMS_PER_PAGE, | ||
media_type: category.mediaType, | ||
search: debouncedSearch, | ||
orderBy: !! debouncedSearch ? 'relevance' : 'date', | ||
} ); | ||
const baseCssClass = 'block-editor-inserter__media-panel'; | ||
return ( | ||
<div className={ baseCssClass }> | ||
<SearchControl | ||
className={ `${ baseCssClass }-search` } | ||
onChange={ setSearch } | ||
value={ search } | ||
label={ sprintf( | ||
/* translators: %s: Name of the media category(ex. 'images, videos'). */ | ||
__( 'Search %s' ), | ||
category.label.toLocaleLowerCase() | ||
) } | ||
placeholder={ sprintf( | ||
/* translators: %s: Name of the media category(ex. 'images, videos'). */ | ||
__( 'Search %s' ), | ||
category.label.toLocaleLowerCase() | ||
) } | ||
/> | ||
{ ! mediaList && ( | ||
<div className={ `${ baseCssClass }-spinner` }> | ||
<Spinner /> | ||
</div> | ||
) } | ||
{ Array.isArray( mediaList ) && ! mediaList.length && ( | ||
<InserterNoResults /> | ||
) } | ||
{ !! mediaList?.length && ( | ||
<MediaList | ||
rootClientId={ rootClientId } | ||
onClick={ onInsert } | ||
mediaList={ mediaList } | ||
mediaType={ category.mediaType } | ||
/> | ||
) } | ||
</div> | ||
); | ||
} |
Oops, something went wrong.