diff --git a/packages/media-utils/src/components/media-upload/index.js b/packages/media-utils/src/components/media-upload/index.js index a822956dd5b34e..e7f8ea47ccd7ef 100644 --- a/packages/media-utils/src/components/media-upload/index.js +++ b/packages/media-utils/src/components/media-upload/index.js @@ -1,17 +1,37 @@ /** * External dependencies */ -import { castArray, defaults, pick } from 'lodash'; +import { castArray, defaults, pick, noop } from 'lodash'; /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; +import { useEffect, useState, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; const { wp } = window; -const DEFAULT_EMPTY_GALLERY = []; +/** + * The media library image object contains numerous attributes + * we only need this set to display the image in the library. + * + * @param {Object} imgObject The image object, whose properties we want to filter. + * + * @return {Array} A filtered image attributes array. + */ +const slimImageObject = ( imgObject ) => { + return pick( imgObject, [ + 'sizes', + 'mime', + 'type', + 'subtype', + 'id', + 'url', + 'alt', + 'link', + 'caption', + ] ); +}; /** * Prepares the Featured Image toolbars and frames. @@ -196,23 +216,13 @@ const getGalleryDetailsMediaFrame = () => { } ); }; -// the media library image object contains numerous attributes -// we only need this set to display the image in the library -const slimImageObject = ( img ) => { - const attrSet = [ - 'sizes', - 'mime', - 'type', - 'subtype', - 'id', - 'url', - 'alt', - 'link', - 'caption', - ]; - return pick( img, attrSet ); -}; - +/** + * Returns a collection of attachments for a given array of post ids. + * + * @param {Array} ids An array of post ids. + * + * @return {Object} A collection of attachments. + */ const getAttachmentsCollection = ( ids ) => { return wp.media.query( { order: 'ASC', @@ -224,78 +234,127 @@ const getAttachmentsCollection = ( ids ) => { } ); }; -class MediaUpload extends Component { - constructor( { - allowedTypes, - gallery = false, - unstableFeaturedImageFlow = false, - modalClass, - multiple = false, - title = __( 'Select or Upload Media' ), - } ) { - super( ...arguments ); - this.openModal = this.openModal.bind( this ); - this.onOpen = this.onOpen.bind( this ); - this.onSelect = this.onSelect.bind( this ); - this.onUpdate = this.onUpdate.bind( this ); - this.onClose = this.onClose.bind( this ); +export default function MediaUpload( { + allowedTypes = [], + gallery = false, + unstableFeaturedImageFlow = false, + modalClass = '', + multiple = false, + title = __( 'Select or Upload Media' ), + onSelect = noop, + render = noop, + onClose = noop, + value = [], + addToGallery = false, +} ) { + const frame = useRef(); + const GalleryDetailsMediaFrame = useRef(); + const [ lastGalleryValue, setLastGalleryValue ] = useState( null ); + + const updateCollection = () => { + const frameContent = frame.current?.content?.get(); + if ( frameContent && frameContent.collection ) { + const collection = frameContent.collection; - if ( gallery ) { - this.buildAndSetGalleryFrame(); - } else { - const frameConfig = { - title, - multiple, - }; - if ( !! allowedTypes ) { - frameConfig.library = { type: allowedTypes }; + // clean all attachments we have in memory. + collection + .toArray() + .forEach( ( model ) => model.trigger( 'destroy', model ) ); + + // reset has more flag, if library had small amount of items all items may have been loaded before. + collection.mirroring._hasMore = true; + + // request items + collection.more(); + } + }; + + const onOpenModal = () => { + updateCollection(); + + // Handle both value being either (number[]) multiple ids + // (for galleries) or a (number) singular id (e.g. image block). + const hasMedia = Array.isArray( value ) ? !! value?.length : !! value; + + if ( ! hasMedia ) { + return; + } + + const isGallery = gallery; + const selection = frame.current?.state().get( 'selection' ); + + if ( ! isGallery ) { + castArray( value ).forEach( ( id ) => { + selection.add( wp.media.attachment( id ) ); + } ); + } + + // Load the images so they are available in the media modal. + const attachments = getAttachmentsCollection( castArray( value ) ); + + // Once attachments are loaded, set the current selection. + attachments.more().done( function () { + if ( isGallery && attachments?.models?.length ) { + selection.add( attachments.models ); } + } ); + }; + const onUpdate = ( selections ) => { + const state = frame.current?.state(); + const selectedImages = selections || state.get( 'selection' ); - this.frame = wp.media( frameConfig ); + if ( ! selectedImages?.models?.length ) { + return; } - if ( modalClass ) { - this.frame.$el.addClass( modalClass ); + if ( multiple ) { + onSelect( + selectedImages.models.map( ( model ) => + slimImageObject( model.toJSON() ) + ) + ); + } else { + onSelect( slimImageObject( selectedImages.models[ 0 ].toJSON() ) ); } + }; - if ( unstableFeaturedImageFlow ) { - this.buildAndSetFeatureImageFrame(); + const onCloseModal = () => { + if ( onClose ) { + onClose(); } - this.initializeListeners(); - } + }; + + const onSelectMedia = () => { + // Get media attachment details from the frame state + const attachment = frame.current?.state().get( 'selection' ).toJSON(); + onSelect( multiple ? attachment : attachment[ 0 ] ); + }; - initializeListeners() { - // When an image is selected in the media frame... - this.frame.on( 'select', this.onSelect ); - this.frame.on( 'update', this.onUpdate ); - this.frame.on( 'open', this.onOpen ); - this.frame.on( 'close', this.onClose ); - } + const initializeListeners = () => { + frame.current?.on( 'select', onSelectMedia ); + frame.current?.on( 'update', onUpdate ); + frame.current?.on( 'open', onOpenModal ); + frame.current?.on( 'close', onCloseModal ); + }; /** * Sets the Gallery frame and initializes listeners. * * @return {void} */ - buildAndSetGalleryFrame() { - const { - addToGallery = false, - allowedTypes, - multiple = false, - value = DEFAULT_EMPTY_GALLERY, - } = this.props; - + const buildAndSetGalleryFrame = () => { // If the value did not changed there is no need to rebuild the frame, // we can continue to use the existing one. - if ( value === this.lastGalleryValue ) { + if ( value === lastGalleryValue ) { return; } - this.lastGalleryValue = value; + setLastGalleryValue( value ); // If a frame already existed remove it. - if ( this.frame ) { - this.frame.remove(); + if ( frame.current ) { + frame.current?.remove(); + frame.current = undefined; } let currentState; if ( addToGallery ) { @@ -303,148 +362,89 @@ class MediaUpload extends Component { } else { currentState = value && value.length ? 'gallery-edit' : 'gallery'; } - if ( ! this.GalleryDetailsMediaFrame ) { - this.GalleryDetailsMediaFrame = getGalleryDetailsMediaFrame(); + + if ( ! GalleryDetailsMediaFrame?.current ) { + GalleryDetailsMediaFrame.current = getGalleryDetailsMediaFrame(); } + const attachments = getAttachmentsCollection( value ); - const selection = new wp.media.model.Selection( attachments.models, { - props: attachments.props.toJSON(), + const selection = new wp.media.model.Selection( attachments?.models, { + props: attachments?.props.toJSON(), multiple, } ); - this.frame = new this.GalleryDetailsMediaFrame( { + + frame.current = new GalleryDetailsMediaFrame.current( { mimeType: allowedTypes, state: currentState, multiple, selection, - editing: value && value.length ? true : false, + editing: !! ( value && value.length ), } ); - wp.media.frame = this.frame; - this.initializeListeners(); - } + wp.media.frame = frame.current; + initializeListeners(); + }; + + const renderOpenModal = () => { + if ( gallery ) { + buildAndSetGalleryFrame(); + } + frame.current?.open(); + }; /** * Initializes the Media Library requirements for the featured image flow. * * @return {void} */ - buildAndSetFeatureImageFrame() { + const buildAndSetFeatureImageFrame = () => { const featuredImageFrame = getFeaturedImageMediaFrame(); - const attachments = getAttachmentsCollection( this.props.value ); + const attachments = getAttachmentsCollection( value ); const selection = new wp.media.model.Selection( attachments.models, { props: attachments.props.toJSON(), } ); - this.frame = new featuredImageFrame( { - mimeType: this.props.allowedTypes, + frame.current = new featuredImageFrame( { + mimeType: allowedTypes, state: 'featured-image', - multiple: this.props.multiple, + multiple, selection, - editing: this.props.value ? true : false, + editing: !! value, } ); - wp.media.frame = this.frame; - } - - componentWillUnmount() { - this.frame.remove(); - } - - onUpdate( selections ) { - const { onSelect, multiple = false } = this.props; - const state = this.frame.state(); - const selectedImages = selections || state.get( 'selection' ); + wp.media.frame = frame.current; + }; - if ( ! selectedImages || ! selectedImages.models.length ) { - return; - } - - if ( multiple ) { - onSelect( - selectedImages.models.map( ( model ) => - slimImageObject( model.toJSON() ) - ) - ); + const initializeMediaUploadFrame = () => { + if ( gallery ) { + buildAndSetGalleryFrame(); } else { - onSelect( slimImageObject( selectedImages.models[ 0 ].toJSON() ) ); - } - } - - onSelect() { - const { onSelect, multiple = false } = this.props; - // Get media attachment details from the frame state - const attachment = this.frame.state().get( 'selection' ).toJSON(); - onSelect( multiple ? attachment : attachment[ 0 ] ); - } - - onOpen() { - this.updateCollection(); - - // Handle both this.props.value being either (number[]) multiple ids - // (for galleries) or a (number) singular id (e.g. image block). - const hasMedia = Array.isArray( this.props.value ) - ? !! this.props.value?.length - : !! this.props.value; + const frameConfig = { + title, + multiple, + }; + if ( !! allowedTypes ) { + frameConfig.library = { type: allowedTypes }; + } - if ( ! hasMedia ) { - return; + frame.current = wp.media( frameConfig ); } - const isGallery = this.props.gallery; - const selection = this.frame.state().get( 'selection' ); - - if ( ! isGallery ) { - castArray( this.props.value ).forEach( ( id ) => { - selection.add( wp.media.attachment( id ) ); - } ); + if ( modalClass ) { + frame.current.$el.addClass( modalClass ); } - // Load the images so they are available in the media modal. - const attachments = getAttachmentsCollection( - castArray( this.props.value ) - ); - - // Once attachments are loaded, set the current selection. - attachments.more().done( function () { - if ( isGallery && attachments?.models?.length ) { - selection.add( attachments.models ); - } - } ); - } - - onClose() { - const { onClose } = this.props; - - if ( onClose ) { - onClose(); + if ( unstableFeaturedImageFlow ) { + buildAndSetFeatureImageFrame(); } - } - - updateCollection() { - const frameContent = this.frame.content.get(); - if ( frameContent && frameContent.collection ) { - const collection = frameContent.collection; - - // clean all attachments we have in memory. - collection - .toArray() - .forEach( ( model ) => model.trigger( 'destroy', model ) ); - - // reset has more flag, if library had small amount of items all items may have been loaded before. - collection.mirroring._hasMore = true; - // request items - collection.more(); - } - } + initializeListeners(); + }; - openModal() { - if ( this.props.gallery ) { - this.buildAndSetGalleryFrame(); - } - this.frame.open(); - } + // Initialize listeners. + useEffect( () => { + initializeMediaUploadFrame(); + return () => { + frame.current?.remove(); + }; + }, [] ); - render() { - return this.props.render( { open: this.openModal } ); - } + return render( { open: renderOpenModal } ); } - -export default MediaUpload;