From c907707bc06b8f5b042f76e26ed593a4fae5fcf8 Mon Sep 17 00:00:00 2001 From: ramonjd Date: Tue, 2 Nov 2021 10:01:51 +1100 Subject: [PATCH 1/3] initial commit --- .../src/components/media-upload/index.js | 360 +++++++++--------- 1 file changed, 190 insertions(+), 170 deletions(-) diff --git a/packages/media-utils/src/components/media-upload/index.js b/packages/media-utils/src/components/media-upload/index.js index a822956dd5b34e..a87dede6e9a20a 100644 --- a/packages/media-utils/src/components/media-upload/index.js +++ b/packages/media-utils/src/components/media-upload/index.js @@ -1,17 +1,43 @@ /** * 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'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as mediaUtilsStore } from '../../store'; 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 +222,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 +240,119 @@ 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, + onRemove = noop, + render = noop, + onClose = noop, + value = [], + addToGallery = false, +} ) { + const frame = useRef(); + const [ lastGalleryValue, setLastGalleryValue ] = useState( null ); + const { removeAttachment } = useDispatch( mediaUtilsStore ); + let GalleryDetailsMediaFrame; + + const renderOpenModal = () => { if ( gallery ) { - this.buildAndSetGalleryFrame(); - } else { - const frameConfig = { - title, - multiple, - }; - if ( !! allowedTypes ) { - frameConfig.library = { type: allowedTypes }; + buildAndSetGalleryFrame(); + } + frame.current?.open(); + }; + + 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 || ! 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(); - } + }; - 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 onRemoveSelectedAttachment = ( attachment ) => { + if ( attachment.destroyed ) { + console.log( 'onRemoveSelectedAttachment', attachment ); + removeAttachment( attachment ); + onRemove( attachment ); + } + }; + + const onSelectMedia = () => { + // Get media attachment details from the frame state + const attachment = frame.current?.state().get( 'selection' ).toJSON(); + onSelect( multiple ? attachment : attachment[ 0 ] ); + }; /** * Sets the Gallery frame and initializes listeners. * * @return {void} */ - buildAndSetGalleryFrame() { - const { - addToGallery = false, - allowedTypes, - multiple = false, - value = DEFAULT_EMPTY_GALLERY, - } = this.props; - + function 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,23 +360,26 @@ class MediaUpload extends Component { } else { currentState = value && value.length ? 'gallery-edit' : 'gallery'; } - if ( ! this.GalleryDetailsMediaFrame ) { - this.GalleryDetailsMediaFrame = getGalleryDetailsMediaFrame(); + + if ( ! GalleryDetailsMediaFrame ) { + GalleryDetailsMediaFrame = 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( { 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(); } /** @@ -327,98 +387,24 @@ class MediaUpload extends Component { * * @return {void} */ - buildAndSetFeatureImageFrame() { + function 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' ); - - if ( ! selectedImages || ! selectedImages.models.length ) { - return; - } - - if ( multiple ) { - onSelect( - selectedImages.models.map( ( model ) => - slimImageObject( model.toJSON() ) - ) - ); - } 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 ] ); + wp.media.frame = frame.current; } - 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; - - if ( ! hasMedia ) { - return; - } - - 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 ) ); - } ); - } - - // 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(); - } - } - - updateCollection() { - const frameContent = this.frame.content.get(); + function updateCollection() { + const frameContent = frame.current?.content?.get(); if ( frameContent && frameContent.collection ) { const collection = frameContent.collection; @@ -435,16 +421,50 @@ class MediaUpload extends Component { } } - openModal() { - if ( this.props.gallery ) { - this.buildAndSetGalleryFrame(); - } - this.frame.open(); + function initializeListeners() { + frame.current?.on( 'select', onSelectMedia ); + frame.current?.on( 'update', onUpdate ); + frame.current?.on( 'open', onOpenModal ); + frame.current?.on( 'close', onCloseModal ); + frame.current?.listenTo( + wp.media.model.Attachments.all, + 'remove', + onRemoveSelectedAttachment + ); } - render() { - return this.props.render( { open: this.openModal } ); + function initializeMediaUploadFrame() { + if ( gallery ) { + buildAndSetGalleryFrame(); + } else { + const frameConfig = { + title, + multiple, + }; + if ( !! allowedTypes ) { + frameConfig.library = { type: allowedTypes }; + } + + frame.current = wp.media( frameConfig ); + } + + if ( modalClass ) { + frame.current.$el.addClass( modalClass ); + } + + if ( unstableFeaturedImageFlow ) { + buildAndSetFeatureImageFrame(); + } + + initializeListeners(); } + // Initialize listeners. + useEffect( () => { + initializeMediaUploadFrame(); + return () => { + frame.current?.remove(); + }; + }, [] ); + + return render( { open: renderOpenModal } ); } - -export default MediaUpload; From c7e6a86c60c71aa7b6f8d56430299e13fcb69fb3 Mon Sep 17 00:00:00 2001 From: ramonjd Date: Mon, 31 Jan 2022 15:15:47 +1100 Subject: [PATCH 2/3] Rolling back changes to the store. Refactoring method order. --- .../src/components/media-upload/index.js | 100 +++++++----------- 1 file changed, 40 insertions(+), 60 deletions(-) diff --git a/packages/media-utils/src/components/media-upload/index.js b/packages/media-utils/src/components/media-upload/index.js index a87dede6e9a20a..489e17eacfd4b2 100644 --- a/packages/media-utils/src/components/media-upload/index.js +++ b/packages/media-utils/src/components/media-upload/index.js @@ -8,12 +8,6 @@ import { castArray, defaults, pick, noop } from 'lodash'; */ import { useEffect, useState, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { store as mediaUtilsStore } from '../../store'; const { wp } = window; @@ -248,22 +242,31 @@ export default function MediaUpload( { multiple = false, title = __( 'Select or Upload Media' ), onSelect = noop, - onRemove = noop, render = noop, onClose = noop, value = [], addToGallery = false, } ) { const frame = useRef(); + const GalleryDetailsMediaFrame = useRef(); const [ lastGalleryValue, setLastGalleryValue ] = useState( null ); - const { removeAttachment } = useDispatch( mediaUtilsStore ); - let GalleryDetailsMediaFrame; - const renderOpenModal = () => { - if ( gallery ) { - buildAndSetGalleryFrame(); + const updateCollection = () => { + const frameContent = frame.current?.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(); } - frame.current?.open(); }; const onOpenModal = () => { @@ -321,26 +324,25 @@ export default function MediaUpload( { } }; - const onRemoveSelectedAttachment = ( attachment ) => { - if ( attachment.destroyed ) { - console.log( 'onRemoveSelectedAttachment', attachment ); - removeAttachment( attachment ); - onRemove( attachment ); - } - }; - const onSelectMedia = () => { // Get media attachment details from the frame state const attachment = frame.current?.state().get( 'selection' ).toJSON(); onSelect( multiple ? attachment : attachment[ 0 ] ); }; + 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} */ - function buildAndSetGalleryFrame() { + 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 === lastGalleryValue ) { @@ -361,8 +363,8 @@ export default function MediaUpload( { currentState = value && value.length ? 'gallery-edit' : 'gallery'; } - if ( ! GalleryDetailsMediaFrame ) { - GalleryDetailsMediaFrame = getGalleryDetailsMediaFrame(); + if ( ! GalleryDetailsMediaFrame?.current ) { + GalleryDetailsMediaFrame.current = getGalleryDetailsMediaFrame(); } const attachments = getAttachmentsCollection( value ); @@ -371,7 +373,7 @@ export default function MediaUpload( { multiple, } ); - frame.current = new GalleryDetailsMediaFrame( { + frame.current = new GalleryDetailsMediaFrame.current( { mimeType: allowedTypes, state: currentState, multiple, @@ -380,14 +382,21 @@ export default function MediaUpload( { } ); 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} */ - function buildAndSetFeatureImageFrame() { + const buildAndSetFeatureImageFrame = () => { const featuredImageFrame = getFeaturedImageMediaFrame(); const attachments = getAttachmentsCollection( value ); const selection = new wp.media.model.Selection( attachments.models, { @@ -401,39 +410,9 @@ export default function MediaUpload( { editing: !! value, } ); wp.media.frame = frame.current; - } - - function updateCollection() { - const frameContent = frame.current?.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(); - } - } + }; - function initializeListeners() { - frame.current?.on( 'select', onSelectMedia ); - frame.current?.on( 'update', onUpdate ); - frame.current?.on( 'open', onOpenModal ); - frame.current?.on( 'close', onCloseModal ); - frame.current?.listenTo( - wp.media.model.Attachments.all, - 'remove', - onRemoveSelectedAttachment - ); - } - - function initializeMediaUploadFrame() { + const initializeMediaUploadFrame = () => { if ( gallery ) { buildAndSetGalleryFrame(); } else { @@ -457,7 +436,8 @@ export default function MediaUpload( { } initializeListeners(); - } + }; + // Initialize listeners. useEffect( () => { initializeMediaUploadFrame(); From b24bed66399f1156605f3e554fa0f8272fba0fcd Mon Sep 17 00:00:00 2001 From: ramonjd Date: Mon, 31 Jan 2022 15:39:10 +1100 Subject: [PATCH 3/3] Checking for selectedImages length --- packages/media-utils/src/components/media-upload/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/media-utils/src/components/media-upload/index.js b/packages/media-utils/src/components/media-upload/index.js index 489e17eacfd4b2..e7f8ea47ccd7ef 100644 --- a/packages/media-utils/src/components/media-upload/index.js +++ b/packages/media-utils/src/components/media-upload/index.js @@ -303,7 +303,7 @@ export default function MediaUpload( { const state = frame.current?.state(); const selectedImages = selections || state.get( 'selection' ); - if ( ! selectedImages || ! selectedImages.models.length ) { + if ( ! selectedImages?.models?.length ) { return; }