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 14, 2022
1 parent 97c48be commit cf8aa92
Show file tree
Hide file tree
Showing 11 changed files with 662 additions and 10 deletions.
135 changes: 135 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,135 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../../store';

// TODO: Even though the `mature` param is `false` by default, we might need to determine where
// and if there is other 'weird' content like in the title. It happened for me to test with `skate`
// and the first result contains a word in the title that might not be suitable for all users.
async function fetchFromOpenverse( { search, pageSize = 10 } ) {
const controller = new AbortController();
const url = new URL( 'https://api.openverse.engineering/v1/images/' );
// TODO: add licence filters etc..
url.searchParams.set( 'q', search );
url.searchParams.set( 'page_size', pageSize );
const response = await window.fetch( url, {
headers: [ [ 'Content-Type', 'application/json' ] ],
signal: controller.signal,
} );
return response.json();
}

export function useMediaResults( category, options = {} ) {
const [ results, setResults ] = useState( [] );
const settings = useSelect(
( select ) => select( blockEditorStore ).getSettings(),
[]
);
const isOpenverse = category.name === 'openverse';
useEffect( () => {
( async () => {
// TODO: add loader probably and not set results..
setResults( [] );
try {
if ( isOpenverse ) {
const response = await fetchFromOpenverse( options );
setResults( response.results );
} else {
const _media = await settings?.__unstableFetchMedia( {
per_page: 7,
media_type: category.mediaType,
} );
if ( _media ) setResults( _media );
}
} catch ( error ) {
// TODO: handle this
throw error;
}
} )();
}, [ category?.name, ...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' },
{ label: 'Openverse', name: 'openverse', mediaType: 'image' },
];
// TODO: definitely revisit the implementation and probably add a loader(return loading state)..
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 () => {
const query = {
context: 'view',
per_page: 1,
_fields: [ 'id' ],
};
const [ showImage, showVideo, showAudio ] = await Promise.all( [
canInsertImage &&
!! (
await fetchMedia( {
...query,
media_type: 'image',
} )
).length,
canInsertVideo &&
!! (
await fetchMedia( {
...query,
media_type: 'video',
} )
).length,
canInsertAudio &&
!! (
await fetchMedia( {
...query,
media_type: '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,2 @@
export { default as MediaTab } from './media-tab';
export { MediaCategoryDialog } from './media-panel';
130 changes: 130 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,130 @@
/**
* WordPress dependencies
*/
// import { useMemo, useCallback } from '@wordpress/element';
// 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
*/
/**
* Internal dependencies
*/
import InserterDraggableBlocks from '../../inserter-draggable-blocks';
import BlockPreview from '../../block-preview';

function getBlocksPreview( media, mediaType, isOpenverse ) {
let attributes;
// TODO: check all the needed attributes(alt, caption, etc..)
if ( mediaType === 'image' ) {
attributes = isOpenverse
? { url: media.thumbnail || media.url }
: {
id: media.id,
url: media.source_url,
};
} else if ( mediaType === 'video' || mediaType === 'audio' ) {
attributes = {
id: media.id,
src: media.source_url,
};
}

const blocks = createBlock( `core/${ mediaType }`, attributes );
return blocks;
}

function MediaPreview( { media, onClick, composite, mediaType, isOpenverse } ) {
// TODO: Check caption or attribution, etc..
// TODO: create different blocks per media type..
const blocks = getBlocksPreview( media, mediaType, isOpenverse );
// 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={ () => {
// TODO: We need to handle the case with focus to image's caption
// during insertion. This makes the inserter to close.
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( {
isOpenverse,
results,
mediaType,
onClick,
label = __( 'Media List' ),
} ) {
const composite = useCompositeState();
// const blocks = useMemo( () => {
// // TODO: Check caption or attribution, etc..
// // TODO: create different blocks per media type..
// // const blockss = getBlocksPreview( media, mediaType, isOpenverse );
// // return blockss;
// }, [] );
// const onClickItem = useCallback( () => {}, [] );
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 }
isOpenverse={ isOpenverse }
/>
) ) }
</Composite>
);
}

export default MediaList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* WordPress dependencies
*/
import { useRef, useEffect } from '@wordpress/element';
import { __experimentalHeading as Heading } from '@wordpress/components';
import { focus } from '@wordpress/dom';

/**
* Internal dependencies
*/
import MediaList from './media-list';
import { useMediaResults } from './hooks';

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 ] );

const results = useMediaResults( category );

// const showOpenverse = category.name === 'openverse';
// TODO: should probably get the media here..(?)
return (
<div ref={ container } className="block-editor-inserter__media-panel">
<MediaCategoryPanel
rootClientId={ rootClientId }
onInsert={ onInsert }
category={ category }
results={ results }
/>
</div>
);
}

export function MediaCategoryPanel( {
rootClientId,
onInsert,
category,
results,
} ) {
// TODO: add loader probably..
return (
<div>
<Heading level="4" weight="400">
{ category.label }
</Heading>
{ !! results.length && (
<MediaList
rootClientId={ rootClientId }
onClick={ onInsert }
results={ results }
mediaType={ category.mediaType }
isOpenverse={ category.name === 'openverse' }
/>
) }
</div>
);
}
Loading

0 comments on commit cf8aa92

Please sign in to comment.