@wordpress/block-library
Version:
Block library for the WordPress editor.
386 lines (378 loc) • 12.4 kB
JavaScript
/**
* External dependencies
*/
import clsx from 'clsx';
/**
* WordPress dependencies
*/
import { isBlobURL } from '@wordpress/blob';
import { useEntityProp, store as coreStore } from '@wordpress/core-data';
import { useSelect, useDispatch } from '@wordpress/data';
import { ToggleControl, Placeholder, Button, Spinner, TextControl, __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem } from '@wordpress/components';
import { InspectorControls, BlockControls, MediaPlaceholder, MediaReplaceFlow, useBlockProps, __experimentalUseBorderProps as useBorderProps, __experimentalGetShadowClassesAndStyles as getShadowClassesAndStyles, useBlockEditingMode, privateApis as blockEditorPrivateApis, store as blockEditorStore } from '@wordpress/block-editor';
import { useMemo, useEffect, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { upload } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
/**
* Internal dependencies
*/
import DimensionControls from './dimension-controls';
import OverlayControls from './overlay-controls';
import Overlay from './overlay';
import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
import { unlock } from '../lock-unlock';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
const ALLOWED_MEDIA_TYPES = ['image'];
const {
ResolutionTool
} = unlock(blockEditorPrivateApis);
const DEFAULT_MEDIA_SIZE_SLUG = 'full';
function FeaturedImageResolutionTool({
image,
value,
onChange
}) {
const {
imageSizes
} = useSelect(select => {
const {
getSettings
} = select(blockEditorStore);
return {
imageSizes: getSettings().imageSizes
};
}, []);
if (!imageSizes?.length) {
return null;
}
const imageSizeOptions = imageSizes.filter(({
slug
}) => image?.media_details?.sizes?.[slug]?.source_url).map(({
name,
slug
}) => ({
value: slug,
label: name
}));
return /*#__PURE__*/_jsx(ResolutionTool, {
value: value,
defaultValue: DEFAULT_MEDIA_SIZE_SLUG,
options: imageSizeOptions,
onChange: onChange
});
}
export default function PostFeaturedImageEdit({
clientId,
attributes,
setAttributes,
context: {
postId,
postType: postTypeSlug,
queryId
}
}) {
const isDescendentOfQueryLoop = Number.isFinite(queryId);
const {
isLink,
aspectRatio,
height,
width,
scale,
sizeSlug,
rel,
linkTarget,
useFirstImageFromPost
} = attributes;
const [temporaryURL, setTemporaryURL] = useState();
const [storedFeaturedImage, setFeaturedImage] = useEntityProp('postType', postTypeSlug, 'featured_media', postId);
// Fallback to post content if no featured image is set.
// This is needed for the "Use first image from post" option.
const [postContent] = useEntityProp('postType', postTypeSlug, 'content', postId);
const featuredImage = useMemo(() => {
if (storedFeaturedImage) {
return storedFeaturedImage;
}
if (!useFirstImageFromPost) {
return;
}
const imageOpener = /<!--\s+wp:(?:core\/)?image\s+(?<attrs>{(?:(?:[^}]+|}+(?=})|(?!}\s+\/?-->).)*)?}\s+)?-->/.exec(postContent);
const imageId = imageOpener?.groups?.attrs && JSON.parse(imageOpener.groups.attrs)?.id;
return imageId;
}, [storedFeaturedImage, useFirstImageFromPost, postContent]);
const {
media,
postType,
postPermalink
} = useSelect(select => {
const {
getMedia,
getPostType,
getEditedEntityRecord
} = select(coreStore);
return {
media: featuredImage && getMedia(featuredImage, {
context: 'view'
}),
postType: postTypeSlug && getPostType(postTypeSlug),
postPermalink: getEditedEntityRecord('postType', postTypeSlug, postId)?.link
};
}, [featuredImage, postTypeSlug, postId]);
const mediaUrl = media?.media_details?.sizes?.[sizeSlug]?.source_url || media?.source_url;
const blockProps = useBlockProps({
style: {
width,
height,
aspectRatio
},
className: clsx({
'is-transient': temporaryURL
})
});
const borderProps = useBorderProps(attributes);
const shadowProps = getShadowClassesAndStyles(attributes);
const blockEditingMode = useBlockEditingMode();
const placeholder = content => {
return /*#__PURE__*/_jsx(Placeholder, {
className: clsx('block-editor-media-placeholder', borderProps.className),
withIllustration: true,
style: {
height: !!aspectRatio && '100%',
width: !!aspectRatio && '100%',
...borderProps.style,
...shadowProps.style
},
children: content
});
};
const onSelectImage = value => {
if (value?.id) {
setFeaturedImage(value.id);
}
if (value?.url && isBlobURL(value.url)) {
setTemporaryURL(value.url);
}
};
// On reset image
const onResetImage = () => {
setAttributes({
isLink: false,
linkTarget: '_self',
rel: '',
sizeSlug: undefined
});
setFeaturedImage(0);
};
// Reset temporary url when media is available.
useEffect(() => {
if (mediaUrl && temporaryURL) {
setTemporaryURL();
}
}, [mediaUrl, temporaryURL]);
const {
createErrorNotice
} = useDispatch(noticesStore);
const onUploadError = message => {
createErrorNotice(message, {
type: 'snackbar'
});
setTemporaryURL();
};
const dropdownMenuProps = useToolsPanelDropdownMenuProps();
const controls = blockEditingMode === 'default' && /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx(InspectorControls, {
group: "color",
children: /*#__PURE__*/_jsx(OverlayControls, {
attributes: attributes,
setAttributes: setAttributes,
clientId: clientId
})
}), /*#__PURE__*/_jsx(InspectorControls, {
group: "dimensions",
children: /*#__PURE__*/_jsx(DimensionControls, {
clientId: clientId,
attributes: attributes,
setAttributes: setAttributes,
media: media
})
}), (featuredImage || isDescendentOfQueryLoop || !postId) && /*#__PURE__*/_jsx(InspectorControls, {
children: /*#__PURE__*/_jsxs(ToolsPanel, {
label: __('Settings'),
resetAll: () => {
setAttributes({
isLink: false,
linkTarget: '_self',
rel: ''
});
},
dropdownMenuProps: dropdownMenuProps,
children: [/*#__PURE__*/_jsx(ToolsPanelItem, {
label: postType?.labels.singular_name ? sprintf(
// translators: %s: Name of the post type e.g: "post".
__('Link to %s'), postType.labels.singular_name) : __('Link to post'),
isShownByDefault: true,
hasValue: () => !!isLink,
onDeselect: () => setAttributes({
isLink: false
}),
children: /*#__PURE__*/_jsx(ToggleControl, {
__nextHasNoMarginBottom: true,
label: postType?.labels.singular_name ? sprintf(
// translators: %s: Name of the post type e.g: "post".
__('Link to %s'), postType.labels.singular_name) : __('Link to post'),
onChange: () => setAttributes({
isLink: !isLink
}),
checked: isLink
})
}), isLink && /*#__PURE__*/_jsx(ToolsPanelItem, {
label: __('Open in new tab'),
isShownByDefault: true,
hasValue: () => '_self' !== linkTarget,
onDeselect: () => setAttributes({
linkTarget: '_self'
}),
children: /*#__PURE__*/_jsx(ToggleControl, {
__nextHasNoMarginBottom: true,
label: __('Open in new tab'),
onChange: value => setAttributes({
linkTarget: value ? '_blank' : '_self'
}),
checked: linkTarget === '_blank'
})
}), isLink && /*#__PURE__*/_jsx(ToolsPanelItem, {
label: __('Link rel'),
isShownByDefault: true,
hasValue: () => !!rel,
onDeselect: () => setAttributes({
rel: ''
}),
children: /*#__PURE__*/_jsx(TextControl, {
__next40pxDefaultSize: true,
__nextHasNoMarginBottom: true,
label: __('Link rel'),
value: rel,
onChange: newRel => setAttributes({
rel: newRel
})
})
}), !!media && /*#__PURE__*/_jsx(FeaturedImageResolutionTool, {
image: media,
value: sizeSlug,
onChange: nextSizeSlug => setAttributes({
sizeSlug: nextSizeSlug
})
})]
})
})]
});
let image;
/**
* A Post Featured Image block should not have image replacement
* or upload options in the following cases:
* - Is placed in a Query Loop. This is a conscious decision to
* prevent content editing of different posts in Query Loop, and
* this could change in the future.
* - Is in a context where it does not have a postId (for example
* in a template or template part).
*/
if (!featuredImage && (isDescendentOfQueryLoop || !postId)) {
return /*#__PURE__*/_jsxs(_Fragment, {
children: [controls, /*#__PURE__*/_jsxs("div", {
...blockProps,
children: [!!isLink ? /*#__PURE__*/_jsx("a", {
href: postPermalink,
target: linkTarget,
children: placeholder()
}) : placeholder(), /*#__PURE__*/_jsx(Overlay, {
attributes: attributes,
setAttributes: setAttributes,
clientId: clientId
})]
})]
});
}
const label = __('Add a featured image');
const imageStyles = {
...borderProps.style,
...shadowProps.style,
height: aspectRatio ? '100%' : height,
width: !!aspectRatio && '100%',
objectFit: !!(height || aspectRatio) && scale
};
/**
* When the post featured image block is placed in a context where:
* - It has a postId (for example in a single post)
* - It is not inside a query loop
* - It has no image assigned yet
* Then display the placeholder with the image upload option.
*/
if (!featuredImage && !temporaryURL) {
image = /*#__PURE__*/_jsx(MediaPlaceholder, {
onSelect: onSelectImage,
accept: "image/*",
allowedTypes: ALLOWED_MEDIA_TYPES,
onError: onUploadError,
placeholder: placeholder,
mediaLibraryButton: ({
open
}) => {
return /*#__PURE__*/_jsx(Button, {
__next40pxDefaultSize: true,
icon: upload,
variant: "primary",
label: label,
showTooltip: true,
tooltipPosition: "top center",
onClick: () => {
open();
}
});
}
});
} else {
// We have a Featured image so show a Placeholder if is loading.
image = !media && !temporaryURL ? placeholder() : /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx("img", {
className: borderProps.className,
src: temporaryURL || mediaUrl,
alt: media && media?.alt_text ? sprintf(
// translators: %s: The image's alt text.
__('Featured image: %s'), media.alt_text) : __('Featured image'),
style: imageStyles
}), temporaryURL && /*#__PURE__*/_jsx(Spinner, {})]
});
}
/**
* When the post featured image block:
* - Has an image assigned
* - Is not inside a query loop
* Then display the image and the image replacement option.
*/
return /*#__PURE__*/_jsxs(_Fragment, {
children: [!temporaryURL && controls, !!media && !isDescendentOfQueryLoop && /*#__PURE__*/_jsx(BlockControls, {
group: "other",
children: /*#__PURE__*/_jsx(MediaReplaceFlow, {
mediaId: featuredImage,
mediaURL: mediaUrl,
allowedTypes: ALLOWED_MEDIA_TYPES,
accept: "image/*",
onSelect: onSelectImage,
onError: onUploadError,
onReset: onResetImage
})
}), /*#__PURE__*/_jsxs("figure", {
...blockProps,
children: [!!isLink ? /*#__PURE__*/_jsx("a", {
href: postPermalink,
target: linkTarget,
children: image
}) : image, /*#__PURE__*/_jsx(Overlay, {
attributes: attributes,
setAttributes: setAttributes,
clientId: clientId
})]
})]
});
}
//# sourceMappingURL=edit.js.map