Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the Openverse Media Library #375

Merged
merged 18 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export default function MediaPlaceholder( {
mediaPreview,
multiple = false,
notices,
onDoubleClick,
onSelect,
placeholder,
style,
Expand All @@ -32,16 +31,12 @@ export default function MediaPlaceholder( {
return null;
}

const onlyAllowsImages = () => {
if ( ! allowedTypes || allowedTypes.length === 0 ) {
return false;
}

return allowedTypes.every(
( allowedType ) => allowedType === 'image' || allowedType.startsWith( 'image/' )
);
};

const onlyAllowsImages = allowedTypes?.every(
( allowedType ) => allowedType === 'image' || allowedType.startsWith( 'image/' )
);
const allowsImages = allowedTypes?.some(
( allowedType ) => allowedType === 'image' || allowedType.startsWith( 'image/' )
);
const [ firstAllowedType ] = allowedTypes;
const isOneType = 1 === allowedTypes.length;
const isAudio = isOneType && 'audio' === firstAllowedType;
Expand All @@ -51,21 +46,24 @@ export default function MediaPlaceholder( {
const defaultRenderPlaceholder = ( content ) => {
let title = labels.title;
if ( title === undefined ) {
if ( isAudio ) {
title = __( 'Audio', 'wporg-patterns' );
} else if ( isImage ) {
if ( isImage ) {
title = __( 'Image', 'wporg-patterns' );
} else if ( isAudio ) {
title = __( 'Audio', 'wporg-patterns' );
} else if ( isVideo ) {
title = __( 'Video', 'wporg-patterns' );
} else {
title = __( 'Media', 'wporg-patterns' );
}
}

const instructions = __(
let instructions = __(
"Patterns are required to use our collection of license-free media. You won't be able to upload or link to any other media in your patterns.",
'wporg-patterns'
);
if ( ! allowsImages ) {
instructions = __( 'The pattern directory does not support this media type yet.', 'wporg-patterns' );
}

const placeholderClassName = classnames( 'block-editor-media-placeholder', className, {
'is-appender': isAppender,
Expand All @@ -78,21 +76,20 @@ export default function MediaPlaceholder( {
instructions={ instructions }
className={ placeholderClassName }
notices={ notices }
onDoubleClick={ onDoubleClick }
preview={ mediaPreview }
style={ style }
>
{ content }
{ allowsImages && content }
{ children }
</Placeholder>
);
};
const renderPlaceholder = placeholder ?? defaultRenderPlaceholder;

const mediaLibraryButton = (
const content = renderPlaceholder(
<MediaUpload
addToGallery={ addToGallery }
gallery={ multiple && onlyAllowsImages() }
gallery={ multiple && onlyAllowsImages }
multiple={ multiple }
onSelect={ onSelect }
allowedTypes={ allowedTypes }
Expand All @@ -109,7 +106,5 @@ export default function MediaPlaceholder( {
) }
/>
);

const content = renderPlaceholder( mediaLibraryButton );
return <MediaUploadCheck fallback={ content }>{ content }</MediaUploadCheck>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* WordPress dependencies
*/
import { __, _n, sprintf } from '@wordpress/i18n';
import { Button } from '@wordpress/components';

export default function OpenverseGridActions( { actions, items, onClear } ) {
return (
<div className="pattern-openverse__footer">
{ items.length ? (
<div className="pattern-openverse__footer-selected">
<div>
<p className="pattern-openverse__footer-selected-label">
{ sprintf(
/* translators: %d: number of items selected. */
_n( '%1$d item selected', '%1$d items selected', items.length, 'wporg-patterns' ),
items.length
) }
</p>
<Button variant="link" isDestructive onClick={ onClear }>
{ __( 'Clear', 'wporg-patterns' ) }
</Button>
</div>
{ items.map( ( item ) => (
<img key={ `thumb-${ item.id }` } src={ item.thumbnail } alt={ item.title } />
) ) }
</div>
) : null }
<div className="pattern-openverse__footer-actions">{ actions }</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
/* eslint-disable @wordpress/no-unsafe-wp-apis -- Composite is OK. */
Button,
__unstableComposite as Composite,
__unstableCompositeItem as CompositeItem,
__unstableUseCompositeState as useCompositeState,
/* eslint-enable @wordpress/no-unsafe-wp-apis */
} from '@wordpress/components';

// Helper function to match an item from a list by ID.
function includesById( list = [], item = {} ) {
return !! list.find( ( { id } ) => id === item.id );
}

export default function OpenverseGridItems( { items, multiple, selected, onSelect } ) {
const composite = useCompositeState();

if ( ! items.length ) {
return null;
}

return (
<Composite
{ ...composite }
className="pattern-openverse__grid-items"
role="listbox"
aria-label={ __( 'Openverse Media', 'wporg-patterns' ) }
aria-multiselectable={ multiple }
>
{ items.map( ( item ) => {
const classes = classnames( {
'pattern-openverse__grid-item': true,
'is-selected': includesById( selected, item ),
} );
return (
<CompositeItem
key={ item.id }
role="option"
as={ Button }
{ ...composite }
label={ item.title }
className={ classes }
onClick={ ( event ) => {
event.preventDefault();
onSelect( item );
} }
aria-selected={ includesById( selected, item ) }
>
<img src={ item.thumbnail } alt={ item.title } />
</CompositeItem>
);
} ) }
</Composite>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/**
* WordPress dependencies
*/
import { __, _n, sprintf } from '@wordpress/i18n';
import { Button, Spinner } from '@wordpress/components';
import { useCallback, useEffect, useState } from '@wordpress/element';
import { useDebounce } from '@wordpress/compose';

/**
* Internal dependencies
*/
import { fetchImages } from './utils';
import OpenverseGridActions from './grid-actions';
import OpenverseGridItems from './grid-items';
import OpenversePagination from './pagination';

function formatImageObject( item ) {
return {
sizes: [],
mime: '',
type: 'image',
subtype: '',
id: null, // @todo Passing item.id triggers an API request to media/[id], but leaving this out makes the `value` prop null (so replacing a selected image does not stay selected), `addToGallery` is null even when gallery items exist, etc.
url: item.url,
alt: '',
link: '',
caption: item.title,
};
}

/* other props: addToGallery, allowedTypes, gallery, value */
export default function OpenverseGrid( { searchTerm, onClose, onSelect, multiple } ) {
const [ debouncedSearchTerm, _setDebouncedSearchTerm ] = useState( searchTerm );
const [ page, setPage ] = useState( 1 );
const setDebouncedSearchTerm = useDebounce( _setDebouncedSearchTerm, 500 );

const [ isLoading, setIsLoading ] = useState( false );
const [ items, setItems ] = useState( [] );
const [ selected, setSelected ] = useState( [] );
const [ total, setTotal ] = useState( 0 );
const [ totalPages, setTotalPages ] = useState( 0 );
const hasItems = items.length > 0;

function setResults( results = [], resultsTotal = 0, pageCount = 0 ) {
setIsLoading( false );
setItems( results );
setTotal( resultsTotal );
setTotalPages( pageCount );
}

// Set up a debounced search term, so we don't query constantly while someone is typing.
useEffect( () => {
setDebouncedSearchTerm( searchTerm );
}, [ searchTerm ] );

// When the search changes, reset back to page 1, and trigger a search.
useEffect( () => {
setPage( 1 );
}, [ debouncedSearchTerm ] );

useEffect( () => {
setIsLoading( true );

if ( ! debouncedSearchTerm ) {
setResults();
return;
}

fetchImages( { searchTerm: debouncedSearchTerm, page: page } )
.then( ( data ) => setResults( data.results, data.result_count, data.page_count ) )
.catch( () => setResults() );
}, [ debouncedSearchTerm, page ] );

const onCommitSelected = useCallback( () => {
if ( ! selected || ! selected.length ) {
return;
}

if ( multiple ) {
onSelect( selected.map( formatImageObject ) );
} else {
onSelect( formatImageObject( selected[ 0 ] ) );
}

onClose();
}, [ selected, multiple ] );

const onClick = useCallback(
( newValue ) => {
const index = selected.indexOf( newValue );
if ( multiple ) {
// Value already in list, remove it.
if ( -1 !== index ) {
setSelected( [
...selected.slice( 0, index ),
...selected.slice( index + 1, selected.length ),
] );
} else {
setSelected( [ ...selected, newValue ] );
}
return;
}
// Value already in list, but not multiple, so set to empty array.
if ( -1 !== index ) {
setSelected( [] );
} else {
setSelected( [ newValue ] );
}
},
[ selected, multiple ]
);

if ( isLoading ) {
return (
<div>
<Spinner />
</div>
);
}

if ( ! debouncedSearchTerm.length ) {
return (
<div className="pattern-openverse__collection-notice">
<p>
{ __(
'Patterns are required to use our collection of license-free media provided by Openverse. You won’t be able to upload or link to any other videos in your patterns.',
'wporg-patterns'
) }
</p>
</div>
);
}

if ( ! hasItems ) {
return (
<div>
<h1 className="pattern-openverse__title">
{ sprintf(
/* translators: %s: media search query */
__( 'No results found for "%s"', 'wporg-patterns' ),
debouncedSearchTerm
) }
</h1>
</div>
);
}

return (
<div className="pattern-openverse__grid">
<h1 className="pattern-openverse__title">
{ debouncedSearchTerm.length
? sprintf(
/* translators: %d: number of results. %s: media search query */
_n(
'%1$s result found for "%2$s"',
'%1$s results found for "%2$s"',
total,
'wporg-patterns'
),
new Intl.NumberFormat().format( total ),
debouncedSearchTerm
)
: sprintf(
/* translators: %d: number of results. */
_n( '%1$s result found', '%1$s results found', total, 'wporg-patterns' ),
new Intl.NumberFormat().format( total )
) }
</h1>
<OpenverseGridItems items={ items } multiple={ multiple } selected={ selected } onSelect={ onClick } />
<OpenversePagination
currentPage={ page }
totalPages={ totalPages }
onNavigation={ ( newValue ) => setPage( newValue ) }
/>
<OpenverseGridActions
items={ selected }
onClear={ () => setSelected( [] ) }
actions={
<>
<Button variant="secondary" onClick={ onClose }>
{ __( 'Cancel', 'wporg-patterns' ) }
</Button>
<Button variant="primary" onClick={ onCommitSelected }>
{ __( 'Add media', 'wporg-patterns' ) }
</Button>
</>
}
/>
</div>
);
}
Loading