Skip to content

Commit

Permalink
[Inserter]: Add media tab
Browse files Browse the repository at this point in the history
  • Loading branch information
ntsekouras committed Oct 18, 2022
1 parent bf4eddb commit bc7d88c
Show file tree
Hide file tree
Showing 12 changed files with 703 additions and 6 deletions.
116 changes: 116 additions & 0 deletions packages/block-editor/src/components/inserter/media-tab/hooks.js
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;
}
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 packages/block-editor/src/components/inserter/media-tab/media-list.js
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;
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>
);
}
Loading

0 comments on commit bc7d88c

Please sign in to comment.