@wordpress/block-library
Version:
Block library for the WordPress editor.
471 lines (459 loc) • 17.4 kB
JavaScript
/**
* External dependencies
*/
import clsx from 'clsx';
/**
* WordPress dependencies
*/
import { useEntityProp, store as coreStore } from '@wordpress/core-data';
import { useEffect, useMemo, useRef } from '@wordpress/element';
import { Placeholder, Spinner } from '@wordpress/components';
import { compose, useResizeObserver } from '@wordpress/compose';
import { withColors, ColorPalette, useBlockProps, useSettings, useInnerBlocksProps, __experimentalUseGradient, store as blockEditorStore, useBlockEditingMode } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import { useSelect, useDispatch } from '@wordpress/data';
import { isBlobURL } from '@wordpress/blob';
import { store as noticesStore } from '@wordpress/notices';
/**
* Internal dependencies
*/
import { attributesFromMedia, IMAGE_BACKGROUND_TYPE, VIDEO_BACKGROUND_TYPE, dimRatioToClass, isContentPositionCenter, getPositionClassName, mediaPosition } from '../shared';
import CoverInspectorControls from './inspector-controls';
import CoverBlockControls from './block-controls';
import CoverPlaceholder from './cover-placeholder';
import ResizableCoverPopover from './resizable-cover-popover';
import { getMediaColor, compositeIsDark, DEFAULT_BACKGROUND_COLOR, DEFAULT_OVERLAY_COLOR } from './color-utils';
import { DEFAULT_MEDIA_SIZE_SLUG } from '../constants';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
function getInnerBlocksTemplate(attributes) {
return [['core/paragraph', {
align: 'center',
placeholder: __('Write title…'),
...attributes
}]];
}
/**
* Is the URL a temporary blob URL? A blob URL is one that is used temporarily while
* the media (image or video) is being uploaded and will not have an id allocated yet.
*
* @param {number} id The id of the media.
* @param {string} url The url of the media.
*
* @return {boolean} Is the URL a Blob URL.
*/
const isTemporaryMedia = (id, url) => !id && isBlobURL(url);
function CoverEdit({
attributes,
clientId,
isSelected,
overlayColor,
setAttributes,
setOverlayColor,
toggleSelection,
context: {
postId,
postType
}
}) {
var _media$media_details$;
const {
contentPosition,
id,
url: originalUrl,
backgroundType: originalBackgroundType,
useFeaturedImage,
dimRatio,
focalPoint,
hasParallax,
isDark,
isRepeated,
minHeight,
minHeightUnit,
alt,
allowedBlocks,
templateLock,
tagName: TagName = 'div',
isUserOverlayColor,
sizeSlug
} = attributes;
const [featuredImage] = useEntityProp('postType', postType, 'featured_media', postId);
const {
getSettings
} = useSelect(blockEditorStore);
const {
__unstableMarkNextChangeAsNotPersistent
} = useDispatch(blockEditorStore);
const {
media
} = useSelect(select => {
return {
media: featuredImage && useFeaturedImage ? select(coreStore).getMedia(featuredImage, {
context: 'view'
}) : undefined
};
}, [featuredImage, useFeaturedImage]);
const mediaUrl = (_media$media_details$ = media?.media_details?.sizes?.[sizeSlug]?.source_url) !== null && _media$media_details$ !== void 0 ? _media$media_details$ : media?.source_url;
// User can change the featured image outside of the block, but we still
// need to update the block when that happens. This effect should only
// run when the featured image changes in that case. All other cases are
// handled in their respective callbacks.
useEffect(() => {
(async () => {
if (!useFeaturedImage) {
return;
}
const averageBackgroundColor = await getMediaColor(mediaUrl);
let newOverlayColor = overlayColor.color;
if (!isUserOverlayColor) {
newOverlayColor = averageBackgroundColor;
__unstableMarkNextChangeAsNotPersistent();
setOverlayColor(newOverlayColor);
}
const newIsDark = compositeIsDark(dimRatio, newOverlayColor, averageBackgroundColor);
__unstableMarkNextChangeAsNotPersistent();
setAttributes({
isDark: newIsDark,
isUserOverlayColor: isUserOverlayColor || false
});
})();
// Update the block only when the featured image changes.
}, [mediaUrl]);
// instead of destructuring the attributes
// we define the url and background type
// depending on the value of the useFeaturedImage flag
// to preview in edit the dynamic featured image
const url = useFeaturedImage ? mediaUrl :
// Ensure the url is not malformed due to sanitization through `wp_kses`.
originalUrl?.replaceAll('&', '&');
const backgroundType = useFeaturedImage ? IMAGE_BACKGROUND_TYPE : originalBackgroundType;
const {
createErrorNotice
} = useDispatch(noticesStore);
const {
gradientClass,
gradientValue
} = __experimentalUseGradient();
const onSelectMedia = async newMedia => {
const mediaAttributes = attributesFromMedia(newMedia);
const isImage = [newMedia?.type, newMedia?.media_type].includes(IMAGE_BACKGROUND_TYPE);
const averageBackgroundColor = await getMediaColor(isImage ? newMedia?.url : undefined);
let newOverlayColor = overlayColor.color;
if (!isUserOverlayColor) {
newOverlayColor = averageBackgroundColor;
setOverlayColor(newOverlayColor);
// Make undo revert the next setAttributes and the previous setOverlayColor.
__unstableMarkNextChangeAsNotPersistent();
}
// Only set a new dimRatio if there was no previous media selected
// to avoid resetting to 50 if it has been explicitly set to 100.
// See issue #52835 for context.
const newDimRatio = originalUrl === undefined && dimRatio === 100 ? 50 : dimRatio;
const newIsDark = compositeIsDark(newDimRatio, newOverlayColor, averageBackgroundColor);
if (backgroundType === IMAGE_BACKGROUND_TYPE && mediaAttributes?.id) {
const {
imageDefaultSize
} = getSettings();
// Try to use the previous selected image size if it's available
// otherwise try the default image size or fallback to full size.
if (sizeSlug && (newMedia?.sizes?.[sizeSlug] || newMedia?.media_details?.sizes?.[sizeSlug])) {
mediaAttributes.sizeSlug = sizeSlug;
mediaAttributes.url = newMedia?.sizes?.[sizeSlug]?.url || newMedia?.media_details?.sizes?.[sizeSlug]?.source_url;
} else if (newMedia?.sizes?.[imageDefaultSize] || newMedia?.media_details?.sizes?.[imageDefaultSize]) {
mediaAttributes.sizeSlug = imageDefaultSize;
mediaAttributes.url = newMedia?.sizes?.[imageDefaultSize]?.url || newMedia?.media_details?.sizes?.[imageDefaultSize]?.source_url;
} else {
mediaAttributes.sizeSlug = DEFAULT_MEDIA_SIZE_SLUG;
}
}
setAttributes({
...mediaAttributes,
focalPoint: undefined,
useFeaturedImage: undefined,
dimRatio: newDimRatio,
isDark: newIsDark,
isUserOverlayColor: isUserOverlayColor || false
});
};
const onClearMedia = () => {
let newOverlayColor = overlayColor.color;
if (!isUserOverlayColor) {
newOverlayColor = DEFAULT_OVERLAY_COLOR;
setOverlayColor(undefined);
// Make undo revert the next setAttributes and the previous setOverlayColor.
__unstableMarkNextChangeAsNotPersistent();
}
const newIsDark = compositeIsDark(dimRatio, newOverlayColor, DEFAULT_BACKGROUND_COLOR);
setAttributes({
url: undefined,
id: undefined,
backgroundType: undefined,
focalPoint: undefined,
hasParallax: undefined,
isRepeated: undefined,
useFeaturedImage: undefined,
isDark: newIsDark
});
};
const onSetOverlayColor = async newOverlayColor => {
const averageBackgroundColor = await getMediaColor(url);
const newIsDark = compositeIsDark(dimRatio, newOverlayColor, averageBackgroundColor);
setOverlayColor(newOverlayColor);
// Make undo revert the next setAttributes and the previous setOverlayColor.
__unstableMarkNextChangeAsNotPersistent();
setAttributes({
isUserOverlayColor: true,
isDark: newIsDark
});
};
const onUpdateDimRatio = async newDimRatio => {
const averageBackgroundColor = await getMediaColor(url);
const newIsDark = compositeIsDark(newDimRatio, overlayColor.color, averageBackgroundColor);
setAttributes({
dimRatio: newDimRatio,
isDark: newIsDark
});
};
const onUploadError = message => {
createErrorNotice(message, {
type: 'snackbar'
});
};
const isUploadingMedia = isTemporaryMedia(id, url);
const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType;
const isVideoBackground = VIDEO_BACKGROUND_TYPE === backgroundType;
const blockEditingMode = useBlockEditingMode();
const hasNonContentControls = blockEditingMode === 'default';
const [resizeListener, {
height,
width
}] = useResizeObserver();
const resizableBoxDimensions = useMemo(() => {
return {
height: minHeightUnit === 'px' ? minHeight : 'auto',
width: 'auto'
};
}, [minHeight, minHeightUnit]);
const minHeightWithUnit = minHeight && minHeightUnit ? `${minHeight}${minHeightUnit}` : minHeight;
const isImgElement = !(hasParallax || isRepeated);
const style = {
minHeight: minHeightWithUnit || undefined
};
const backgroundImage = url ? `url(${url})` : undefined;
const backgroundPosition = mediaPosition(focalPoint);
const bgStyle = {
backgroundColor: overlayColor.color
};
const mediaStyle = {
objectPosition: focalPoint && isImgElement ? mediaPosition(focalPoint) : undefined
};
const hasBackground = !!(url || overlayColor.color || gradientValue);
const hasInnerBlocks = useSelect(select => select(blockEditorStore).getBlock(clientId).innerBlocks.length > 0, [clientId]);
const ref = useRef();
const blockProps = useBlockProps({
ref
});
// Check for fontSize support before we pass a fontSize attribute to the innerBlocks.
const [fontSizes] = useSettings('typography.fontSizes');
const hasFontSizes = fontSizes?.length > 0;
const innerBlocksTemplate = getInnerBlocksTemplate({
fontSize: hasFontSizes ? 'large' : undefined
});
const innerBlocksProps = useInnerBlocksProps({
className: 'wp-block-cover__inner-container'
}, {
// Avoid template sync when the `templateLock` value is `all` or `contentOnly`.
// See: https://github.com/WordPress/gutenberg/pull/45632
template: !hasInnerBlocks ? innerBlocksTemplate : undefined,
templateInsertUpdatesSelection: true,
allowedBlocks,
templateLock,
dropZoneElement: ref.current
});
const mediaElement = useRef();
const currentSettings = {
isVideoBackground,
isImageBackground,
mediaElement,
hasInnerBlocks,
url,
isImgElement,
overlayColor
};
const toggleUseFeaturedImage = async () => {
const newUseFeaturedImage = !useFeaturedImage;
const averageBackgroundColor = newUseFeaturedImage ? await getMediaColor(mediaUrl) : DEFAULT_BACKGROUND_COLOR;
const newOverlayColor = !isUserOverlayColor ? averageBackgroundColor : overlayColor.color;
if (!isUserOverlayColor) {
if (newUseFeaturedImage) {
setOverlayColor(newOverlayColor);
} else {
setOverlayColor(undefined);
}
// Make undo revert the next setAttributes and the previous setOverlayColor.
__unstableMarkNextChangeAsNotPersistent();
}
const newDimRatio = dimRatio === 100 ? 50 : dimRatio;
const newIsDark = compositeIsDark(newDimRatio, newOverlayColor, averageBackgroundColor);
setAttributes({
id: undefined,
url: undefined,
useFeaturedImage: newUseFeaturedImage,
dimRatio: newDimRatio,
backgroundType: useFeaturedImage ? IMAGE_BACKGROUND_TYPE : undefined,
isDark: newIsDark
});
};
const blockControls = /*#__PURE__*/_jsx(CoverBlockControls, {
attributes: attributes,
setAttributes: setAttributes,
onSelectMedia: onSelectMedia,
currentSettings: currentSettings,
toggleUseFeaturedImage: toggleUseFeaturedImage,
onClearMedia: onClearMedia
});
const inspectorControls = /*#__PURE__*/_jsx(CoverInspectorControls, {
attributes: attributes,
setAttributes: setAttributes,
clientId: clientId,
setOverlayColor: onSetOverlayColor,
coverRef: ref,
currentSettings: currentSettings,
toggleUseFeaturedImage: toggleUseFeaturedImage,
updateDimRatio: onUpdateDimRatio,
onClearMedia: onClearMedia,
featuredImage: media
});
const resizableCoverProps = {
className: 'block-library-cover__resize-container',
clientId,
height,
minHeight: minHeightWithUnit,
onResizeStart: () => {
setAttributes({
minHeightUnit: 'px'
});
toggleSelection(false);
},
onResize: value => {
setAttributes({
minHeight: value
});
},
onResizeStop: newMinHeight => {
toggleSelection(true);
setAttributes({
minHeight: newMinHeight
});
},
// Hide the resize handle if an aspect ratio is set, as the aspect ratio takes precedence.
showHandle: !attributes.style?.dimensions?.aspectRatio,
size: resizableBoxDimensions,
width
};
if (!useFeaturedImage && !hasInnerBlocks && !hasBackground) {
return /*#__PURE__*/_jsxs(_Fragment, {
children: [blockControls, inspectorControls, hasNonContentControls && isSelected && /*#__PURE__*/_jsx(ResizableCoverPopover, {
...resizableCoverProps
}), /*#__PURE__*/_jsxs(TagName, {
...blockProps,
className: clsx('is-placeholder', blockProps.className),
style: {
...blockProps.style,
minHeight: minHeightWithUnit || undefined
},
children: [resizeListener, /*#__PURE__*/_jsx(CoverPlaceholder, {
onSelectMedia: onSelectMedia,
onError: onUploadError,
toggleUseFeaturedImage: toggleUseFeaturedImage,
children: /*#__PURE__*/_jsx("div", {
className: "wp-block-cover__placeholder-background-options",
children: /*#__PURE__*/_jsx(ColorPalette, {
disableCustomColors: true,
value: overlayColor.color,
onChange: onSetOverlayColor,
clearable: false,
asButtons: true,
"aria-label": __('Overlay color')
})
})
})]
})]
});
}
const classes = clsx({
'is-dark-theme': isDark,
'is-light': !isDark,
'is-transient': isUploadingMedia,
'has-parallax': hasParallax,
'is-repeated': isRepeated,
'has-custom-content-position': !isContentPositionCenter(contentPosition)
}, getPositionClassName(contentPosition));
const showOverlay = url || !useFeaturedImage || useFeaturedImage && !url;
return /*#__PURE__*/_jsxs(_Fragment, {
children: [blockControls, inspectorControls, /*#__PURE__*/_jsxs(TagName, {
...blockProps,
className: clsx(classes, blockProps.className),
style: {
...style,
...blockProps.style
},
"data-url": url,
children: [resizeListener, !url && useFeaturedImage && /*#__PURE__*/_jsx(Placeholder, {
className: "wp-block-cover__image--placeholder-image",
withIllustration: true
}), url && isImageBackground && (isImgElement ? /*#__PURE__*/_jsx("img", {
ref: mediaElement,
className: "wp-block-cover__image-background",
alt: alt,
src: url,
style: mediaStyle
}) : /*#__PURE__*/_jsx("div", {
ref: mediaElement,
role: alt ? 'img' : undefined,
"aria-label": alt ? alt : undefined,
className: clsx(classes, 'wp-block-cover__image-background'),
style: {
backgroundImage,
backgroundPosition
}
})), url && isVideoBackground && /*#__PURE__*/_jsx("video", {
ref: mediaElement,
className: "wp-block-cover__video-background",
autoPlay: true,
muted: true,
loop: true,
src: url,
style: mediaStyle
}), showOverlay && /*#__PURE__*/_jsx("span", {
"aria-hidden": "true",
className: clsx('wp-block-cover__background', dimRatioToClass(dimRatio), {
[overlayColor.class]: overlayColor.class,
'has-background-dim': dimRatio !== undefined,
// For backwards compatibility. Former versions of the Cover Block applied
// `.wp-block-cover__gradient-background` in the presence of
// media, a gradient and a dim.
'wp-block-cover__gradient-background': url && gradientValue && dimRatio !== 0,
'has-background-gradient': gradientValue,
[gradientClass]: gradientClass
}),
style: {
backgroundImage: gradientValue,
...bgStyle
}
}), isUploadingMedia && /*#__PURE__*/_jsx(Spinner, {}), /*#__PURE__*/_jsx(CoverPlaceholder, {
disableMediaButtons: true,
onSelectMedia: onSelectMedia,
onError: onUploadError,
toggleUseFeaturedImage: toggleUseFeaturedImage
}), /*#__PURE__*/_jsx("div", {
...innerBlocksProps
})]
}), hasNonContentControls && isSelected && /*#__PURE__*/_jsx(ResizableCoverPopover, {
...resizableCoverProps
})]
});
}
export default compose([withColors({
overlayColor: 'background-color'
})])(CoverEdit);
//# sourceMappingURL=index.js.map