I managed to somewhat replicate the product gallery metabox from WooCommerce in Gutenberg using the third-party react-sortable-hoc component. As it seems that this component is no longer supported, I'd like to know if one can get the same drag-to-sort functionality using core Gutenberg components only as I prefer to not rely on third-party components for this. Is the core Draggable component an equivalent?
This is the code I got right now:
index.js
const { __ } = wp.i18n;
const {
BaseControl,
Button,
withNotices,
} = wp.components;
const { compose } = wp.compose;
const { withSelect, withDispatch } = wp.data;
const { MediaUpload, MediaUploadCheck } = wp.blockEditor;
import Sortable from './sortable';
import FeaturedGalleryImage from './featured-gallery-image';
const ALLOWED_MEDIA_TYPES = [ 'image' ];
function FeaturedGallery( {
currentPostId,
featuredGalleryIds,
onUpdateGallery,
onSortGallery,
onClearGallery,
noticeUI,
} ) {
const instructions = (
<p>
{ __(
'To edit the featured gallery, you need permission to upload media.', 'my-featured-gallery'
) }
</p>
);
const hasImages = !! featuredGalleryIds.length;
return (
<BaseControl
className="my-featured-gallery"
>
{ noticeUI }
<div className="editor-post-featured-gallery">
<div className="editor-post-featured-gallery__container">
{ hasImages && (
<Sortable
className="featured-gallery-grid"
items={ featuredGalleryIds }
axis="grid"
onSortEnd={ onSortGallery }
>
{ featuredGalleryIds.map( ( img ) => (
<li key={ img } tabIndex={0}>
<FeaturedGalleryImage
id={ img }
/>
</li>
) ) }
</Sortable>
) }
</div>
<MediaUploadCheck fallback={ instructions }>
<MediaUpload
title={ __( 'Featured gallery', 'my-featured-gallery' ) }
multiple
onSelect={ onUpdateGallery }
allowedTypes={ ALLOWED_MEDIA_TYPES }
value={ hasImages ? featuredGalleryIds : [] }
render={ ( { open } ) => (
<Button
className={
hasImages
? 'editor-post-featured-gallery__edit'
: 'editor-post-featured-gallery__add'
}
onClick={ open }
isSecondary
>
{
hasImages
? __( 'Edit gallery', 'my-featured-gallery' )
: __( 'Add to gallery', 'my-featured-gallery' )
}
</Button>
) }
/>
</MediaUploadCheck>
{ hasImages && (
<MediaUploadCheck>
<Button onClick={ onClearGallery } isLink isDestructive>
{
__( 'Clear gallery', 'my-featured-gallery' )
}
</Button>
</MediaUploadCheck>
) }
</div>
</BaseControl>
);
}
const applyWithSelect = withSelect( ( select ) => {
const { getPostType } = select( 'core' );
const { getCurrentPostId, getEditedPostAttribute } = select(
'core/editor'
);
const meta = getEditedPostAttribute( 'meta' );
const featuredGalleryIds = meta._featured_gallery;
return {
currentPostId: getCurrentPostId(),
postType: getPostType( getEditedPostAttribute( 'type' ) ),
featuredGalleryIds,
};
} );
const applyWithDispatch = withDispatch( ( dispatch ) => {
const { editPost } = dispatch( 'core/editor' );
return {
onUpdateGallery( images ) {
const items = images.map( ( item ) => item.id );
const meta = { _featured_gallery: items };
editPost( { meta } );
},
onSortGallery( images ) {
const meta = { _featured_gallery: images };
editPost( { meta } );
},
onClearGallery() {
const meta = { _featured_gallery: [] };
editPost( { meta } );
}
};
}
);
export default compose(
withNotices,
applyWithSelect,
applyWithDispatch,
)( FeaturedGallery );
featured-gallery-image.js
const { __ } = wp.i18n;
const { withSelect } = wp.data;
const { Spinner } = wp.components;
const { compose } = wp.compose;
function FeaturedGalleryImage( {
id,
image,
} ) {
return (
<figure>
{ image && (
<img
src={ image.media_details.sizes.thumbnail.source_url }
width={ image.media_details.sizes.thumbnail.width }
height={ image.media_details.sizes.thumbnail.height }
alt={ __( 'Thumbnail of the image.', 'my-featured-gallery' ) }
/>
) }
{ ! image && (
<Spinner />
) }
</figure>
);
}
const applyWithSelect = withSelect( ( select, ownProps ) => {
const { getMedia } = select( 'core' );
const { id } = ownProps;
return {
image: id ? getMedia( parseInt( id, 10 ) ) : null,
};
} );
export default compose( [
applyWithSelect
] )( FeaturedGalleryImage );
sortable.js
import { Component } from '@wordpress/element';
import { SortableContainer, SortableElement, arrayMove } from 'react-sortable-hoc';
const SortableItem = SortableElement( ( { children } ) => children );
class Sortable extends Component {
constructor() {
super( ...arguments );
this.onSortStart = this.onSortStart.bind( this );
this.onSortEnd = this.onSortEnd.bind( this );
this.state = {
justSorted: false,
};
}
/**
* Get the sortable list:
* @return {function} the Container creator
*/
getSortableList() {
const { children, className, firstNode, lastNode } = this.props;
// Create the sortable container
return SortableContainer( () => {
// Loop through all available children
return (
<ul className={ `components-sortable ${ className }` }>
{ firstNode }
{ children.map( ( child, index ) => {
// Display Sortable element
return (
<SortableItem key={ `item-${ index }` } index={ index }>
{ child }
</SortableItem>
);
} ) }
{ lastNode }
</ul>
);
} );
}
shouldComponentUpdate( nextProps ) {
const should = nextProps.items.every( ( item, index ) => {
return this.props.items[ index ] === item;
} );
const justSorted = this.state.justSorted;
if ( justSorted ) {
this.setState( {
justSorted: false,
} );
}
const lengthChanged = nextProps.items.length !== this.props.items.length;
return should || justSorted || lengthChanged;
}
render() {
const items = this.props.items;
const SortableList = this.getSortableList();
return (
// Return the sortable list, with props from our upper-lever component
<SortableList
axis="xy"
items={ items }
onSortStart={ this.onSortStart }
onSortEnd={ this.onSortEnd }
distance={ 0 }
helperClass={ 'dragged-element' }
/>
);
}
/**
* What to do on sort start ?
* @param {Object} object with access to the element, its index and the collection
* @param {Event} event with the event info
*/
onSortStart( { node, index, collection }, event ) {
// Run the corresponding function in the upper-lever component
if ( typeof ( this.props.onSortStart ) === 'function' ) {
this.props.onSortStart( { node, index, collection }, event );
}
}
/**
* What to do on sort end?
*
* @param {Object} object holding old and new indexes and the collection
*/
onSortEnd( { oldIndex, newIndex } ) {
// Create a new items array
const _items = arrayMove( this.props.items, oldIndex, newIndex );
this.setState( {
justSorted: true,
} );
// And run the corresponding function in the upper-lever component:
if ( typeof ( this.props.onSortEnd ) === 'function' ) {
this.props.onSortEnd( _items );
}
}
}
export default Sortable;