Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ item (Added, Changed, Depreciated, Removed, Fixed, Security).

- Updated: Refactor Horizontal Tabs block into a dynamic block. Add reordering functionality.
- Updated: Refactor Vertical Tabs block to dynamic block; allow sorting.
- Updated: Image / Image Overlay blocks now use a shared `MediaImageControl` component instead of adding the media control separately.
- Updated: Admin menu order should now properly exclude predefined post types
- Updated: Sticky Column block setting now respects masthead height.
- Updated: Button block set to "Default" style now respects the "Width" setting.
Expand Down
1 change: 1 addition & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ module.exports = {
components: resolve(
'./wp-content/themes/core/assets/js/components'
),
hooks: resolve( './wp-content/themes/core/assets/js/hooks' ),
},
},
entry: {
Expand Down
160 changes: 160 additions & 0 deletions wp-content/themes/core/assets/js/components/MediaImageControl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { __ } from '@wordpress/i18n';
import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import {
BaseControl,
Button,
Flex,
FlexItem,
ResponsiveWrapper,
Spinner,
VisuallyHidden,
} from '@wordpress/components';
import { useBlockMedia } from 'hooks/useBlockMedia';

const previewTriggerStyle = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
minHeight: '120px',
boxSizing: 'border-box',
};

const missingMessageStyle = {
textAlign: 'center',
maxWidth: '100%',
lineHeight: 1.4,
padding: '0.5rem 0.25rem',
};

/**
* Inspector control for choosing, previewing, replacing, and removing an image
* from the media library (featured-image-style UI). Loads attachment data via
* `useBlockMedia`; pass only `mediaId` from block attributes.
*
* @param {Object} props
* @param {number|string} props.mediaId Attachment ID, or 0 when empty (coerced like `useBlockMedia`).
* @param {Function} props.onSelect Called with the selected media object from MediaUpload.
* @param {Function} props.onRemove Called when the user removes the image.
* @param {string} [props.label] Visual label for the control.
* @param {string[]} [props.allowedTypes] Passed to MediaUpload.
* @param {string} [props.chooseLabel] Empty-state button text.
* @param {string} [props.replaceLabel] Replace flow label and button text.
* @param {string} [props.removeLabel] Remove button text.
* @param {string} [props.missingLabel] Shown when the ID is set but the attachment cannot be loaded.
*/
export default function MediaImageControl( {
mediaId,
onSelect,
onRemove,
label = __( 'Image', 'tribe' ),
allowedTypes = [ 'image' ],
chooseLabel = __( 'Choose an image', 'tribe' ),
replaceLabel = __( 'Replace image', 'tribe' ),
removeLabel = __( 'Remove image', 'tribe' ),
missingLabel = __(
'Could not load image data. Try replacing the image.',
'tribe'
),
} ) {
const { media, isLoading, isMissing } = useBlockMedia( mediaId );

const width = media?.media_details?.width;
const height = media?.media_details?.height;
const src = media?.source_url;
const altFromMedia = media?.alt_text ?? media?.media_details?.alt ?? '';
const previewAlt = altFromMedia || __( 'Selected image', 'tribe' );

const previewImage =
src &&
( width && height ? (
<ResponsiveWrapper naturalWidth={ width } naturalHeight={ height }>
<img src={ src } alt={ previewAlt } />
</ResponsiveWrapper>
) : (
<img src={ src } alt={ previewAlt } />
) );

return (
<BaseControl __nextHasNoMarginBottom>
<BaseControl.VisualLabel>{ label }</BaseControl.VisualLabel>
<MediaUploadCheck>
<MediaUpload
allowedTypes={ allowedTypes }
onSelect={ onSelect }
value={ mediaId }
render={ ( { open } ) => (
<Button
className={
mediaId === 0
? 'editor-post-featured-image__toggle'
: 'editor-post-featured-image__preview'
}
style={
mediaId !== 0 ? previewTriggerStyle : undefined
}
onClick={ open }
aria-busy={ mediaId !== 0 && isLoading }
>
{ mediaId === 0 && chooseLabel }
{ mediaId !== 0 && isLoading && (
<>
<VisuallyHidden as="span">
{ __( 'Loading image', 'tribe' ) }
</VisuallyHidden>
<Spinner />
</>
) }
{ mediaId !== 0 && ! isLoading && isMissing && (
<span style={ missingMessageStyle }>
{ missingLabel }
</span>
) }
{ mediaId !== 0 &&
! isLoading &&
! isMissing &&
previewImage }
</Button>
) }
/>
</MediaUploadCheck>
{ mediaId !== 0 && (
<Flex
style={ {
marginTop: '1rem',
} }
>
<FlexItem>
<MediaUploadCheck>
<MediaUpload
title={ replaceLabel }
value={ mediaId }
onSelect={ onSelect }
allowedTypes={ allowedTypes }
render={ ( { open } ) => (
<Button
onClick={ open }
variant="secondary"
>
{ replaceLabel }
</Button>
) }
/>
</MediaUploadCheck>
</FlexItem>
<FlexItem>
<MediaUploadCheck>
<Button
variant="link"
isDestructive
onClick={ onRemove }
>
{ removeLabel }
</Button>
</MediaUploadCheck>
</FlexItem>
</Flex>
) }
</BaseControl>
);
}
65 changes: 65 additions & 0 deletions wp-content/themes/core/assets/js/hooks/useBlockMedia.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useSelect } from '@wordpress/data';

/**
* REST query passed to `getEntityRecord` for attachment posts. Kept as a stable
* reference so `hasFinishedResolution` matches the resolver cache key (same
* pattern as core’s post featured image control).
*/
const ATTACHMENT_RECORD_QUERY = { context: 'view' };

/**
* Subscribe to a media attachment from the `core` data store via
* `getEntityRecord( 'postType', 'attachment', id, query )`.
*
* Uses the attachment post type instead of the legacy `getMedia` shortcut so
* this stays aligned with current Gutenberg data APIs.
*
* @param {number|string} mediaId Attachment ID from block attributes, or 0 / empty when none. Numeric strings are coerced.
* @return {{ media: Object|undefined|null, hasResolved: boolean, isLoading: boolean, isMissing: boolean }} `media` is `undefined` while loading; falsy after resolution if the attachment is missing or was deleted.
*/
export function useBlockMedia( mediaId ) {
let id = 0;
if ( typeof mediaId === 'number' && mediaId > 0 ) {
id = mediaId;
} else {
const n = Number( mediaId );
if ( n > 0 ) {
id = n;
}
}

return useSelect(
( select ) => {
if ( ! id ) {
return {
media: undefined,
hasResolved: true,
isLoading: false,
isMissing: false,
};
}

const { getEntityRecord, hasFinishedResolution } = select( 'core' );
const media = getEntityRecord(
'postType',
'attachment',
id,
ATTACHMENT_RECORD_QUERY
);
const hasResolved = hasFinishedResolution( 'getEntityRecord', [
'postType',
'attachment',
id,
ATTACHMENT_RECORD_QUERY,
] );

return {
media,
hasResolved,
isLoading: ! hasResolved,
isMissing: hasResolved && ! media,
};
},
[ id ]
);
}
Loading
Loading