-
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.
- Loading branch information
1 parent
bf4eddb
commit bc7d88c
Showing
12 changed files
with
703 additions
and
6 deletions.
There are no files selected for viewing
116 changes: 116 additions & 0 deletions
116
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,116 @@ | ||
/** | ||
* 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(); | ||
try { | ||
const _media = await settings?.__unstableFetchMedia( options ); | ||
if ( _media ) setResults( _media ); | ||
} catch ( error ) { | ||
// TODO: handle this | ||
throw error; | ||
} | ||
} )(); | ||
}, [ ...Object.values( options ) ] ); | ||
return results; | ||
} | ||
|
||
// TODO: Need to think of the props.. :) | ||
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'; |
111 changes: 111 additions & 0 deletions
111
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,111 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
// import { useInstanceId } from '@wordpress/compose'; | ||
import { | ||
__unstableComposite as Composite, | ||
__unstableUseCompositeState as useCompositeState, | ||
__unstableCompositeItem as CompositeItem, | ||
} from '@wordpress/components'; | ||
import { createBlock } from '@wordpress/blocks'; | ||
import { __ } from '@wordpress/i18n'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import InserterDraggableBlocks from '../../inserter-draggable-blocks'; | ||
import BlockPreview from '../../block-preview'; | ||
|
||
function getBlocksPreview( media, mediaType ) { | ||
let attributes; | ||
// TODO: check all the needed attributes(alt, caption, etc..) | ||
if ( mediaType === 'image' ) { | ||
attributes = { | ||
id: media.id, | ||
url: media.source_url, | ||
caption: media.caption?.rendered || undefined, | ||
alt: media.alt_text, | ||
}; | ||
} else if ( mediaType === 'video' || mediaType === 'audio' ) { | ||
attributes = { | ||
id: media.id, | ||
src: media.source_url, | ||
}; | ||
} | ||
return createBlock( `core/${ mediaType }`, attributes ); | ||
} | ||
|
||
function MediaPreview( { media, onClick, composite, mediaType } ) { | ||
const blocks = getBlocksPreview( media, mediaType ); | ||
// TODO: we have to set a max height for previews as the image can be very tall. | ||
// Probably a fixed-max height for all(?). | ||
const title = media.title?.rendered || media.title; | ||
const baseCssClass = 'block-editor-inserter__media-list'; | ||
// const descriptionId = useInstanceId( | ||
// MediaPreview, | ||
// `${ baseCssClass }__item-description` | ||
// ); | ||
return ( | ||
<InserterDraggableBlocks isEnabled={ true } blocks={ [ blocks ] }> | ||
{ ( { draggable, onDragStart, onDragEnd } ) => ( | ||
<div | ||
className={ `${ baseCssClass }__list-item` } | ||
draggable={ draggable } | ||
onDragStart={ onDragStart } | ||
onDragEnd={ onDragEnd } | ||
> | ||
<CompositeItem | ||
role="option" | ||
as="div" | ||
{ ...composite } | ||
className={ `${ baseCssClass }__item` } | ||
onClick={ () => { | ||
onClick( blocks ); | ||
} } | ||
aria-label={ title } | ||
// aria-describedby={} | ||
> | ||
<BlockPreview blocks={ blocks } viewportWidth={ 400 } /> | ||
<div className={ `${ baseCssClass }__item-title` }> | ||
{ title } | ||
</div> | ||
{ /* { !! description && ( | ||
<VisuallyHidden id={ descriptionId }> | ||
{ description } | ||
</VisuallyHidden> | ||
) } */ } | ||
</CompositeItem> | ||
</div> | ||
) } | ||
</InserterDraggableBlocks> | ||
); | ||
} | ||
|
||
function MediaList( { | ||
results, | ||
mediaType, | ||
onClick, | ||
label = __( 'Media List' ), | ||
} ) { | ||
const composite = useCompositeState(); | ||
return ( | ||
<Composite | ||
{ ...composite } | ||
role="listbox" | ||
className="block-editor-inserter__media-list" | ||
aria-label={ label } | ||
> | ||
{ results.map( ( media ) => ( | ||
<MediaPreview | ||
key={ media.id } | ||
media={ media } | ||
mediaType={ mediaType } | ||
onClick={ onClick } | ||
composite={ composite } | ||
/> | ||
) ) } | ||
</Composite> | ||
); | ||
} | ||
|
||
export default MediaList; |
89 changes: 89 additions & 0 deletions
89
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,89 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useRef, useEffect } from '@wordpress/element'; | ||
import { Spinner, SearchControl } from '@wordpress/components'; | ||
import { focus } from '@wordpress/dom'; | ||
import { useAsyncList } from '@wordpress/compose'; | ||
import { __, sprintf } from '@wordpress/i18n'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import MediaList from './media-list'; | ||
import { useMediaResults, useDebouncedInput } from './hooks'; | ||
import InserterNoResults from '../no-results'; | ||
|
||
const EMPTY_ARRAY = []; | ||
|
||
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 }> | ||
<MediaCategoryPanel | ||
rootClientId={ rootClientId } | ||
onInsert={ onInsert } | ||
category={ category } | ||
/> | ||
</div> | ||
); | ||
} | ||
|
||
const INITIAL_MEDIA_ITEMS_PER_PAGE = 10; | ||
export function MediaCategoryPanel( { rootClientId, onInsert, category } ) { | ||
const [ search, setSearch, debouncedSearch ] = useDebouncedInput(); | ||
const results = useMediaResults( { | ||
per_page: !! debouncedSearch ? 20 : INITIAL_MEDIA_ITEMS_PER_PAGE, | ||
media_type: category.mediaType, | ||
search: debouncedSearch, | ||
orderBy: !! debouncedSearch ? 'relevance' : 'date', | ||
} ); | ||
const shownResults = useAsyncList( results || EMPTY_ARRAY, { | ||
step: 3, | ||
} ); | ||
const baseCssClass = 'block-editor-inserter__media-panel'; | ||
return ( | ||
<div className={ baseCssClass }> | ||
{ shownResults !== undefined && ( | ||
<SearchControl | ||
className={ `${ baseCssClass }-search` } | ||
onChange={ setSearch } | ||
value={ search } | ||
label={ sprintf( | ||
/* translators: %s: Name of the media category(ex. 'Images, Videos'). */ | ||
__( 'Search %s' ), | ||
category.label | ||
) } | ||
placeholder={ sprintf( | ||
/* translators: %s: Name of the media category(ex. 'Images, Videos'). */ | ||
__( 'Search %s' ), | ||
category.label | ||
) } | ||
/> | ||
) } | ||
{ ! results && ( | ||
<div className={ `${ baseCssClass }-spinner` }> | ||
<Spinner /> | ||
</div> | ||
) } | ||
{ Array.isArray( results ) && ! results.length && ( | ||
<InserterNoResults /> | ||
) } | ||
{ !! shownResults?.length && ( | ||
<MediaList | ||
rootClientId={ rootClientId } | ||
onClick={ onInsert } | ||
results={ shownResults } | ||
mediaType={ category.mediaType } | ||
/> | ||
) } | ||
</div> | ||
); | ||
} |
Oops, something went wrong.