@wordpress/block-library
Version:
Block library for the WordPress editor.
922 lines (911 loc) • 33.3 kB
JavaScript
/**
* WordPress dependencies
*/
import { isBlobURL } from '@wordpress/blob';
import { ExternalLink, ResizableBox, Spinner, TextareaControl, TextControl, ToolbarButton, ToolbarGroup, __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, __experimentalUseCustomUnits as useCustomUnits, Placeholder, MenuItem, ToolbarItem, DropdownMenu, Popover } from '@wordpress/components';
import { useMergeRefs, useResizeObserver, useViewportMatch } from '@wordpress/compose';
import { useSelect, useDispatch } from '@wordpress/data';
import { BlockControls, InspectorControls, __experimentalImageURLInputUI as ImageURLInputUI, MediaReplaceFlow, store as blockEditorStore, useSettings, __experimentalImageEditor as ImageEditor, __experimentalUseBorderProps as useBorderProps, __experimentalGetShadowClassesAndStyles as getShadowClassesAndStyles, privateApis as blockEditorPrivateApis, BlockSettingsMenuControls } from '@wordpress/block-editor';
import { useCallback, useEffect, useMemo, useState } from '@wordpress/element';
import { __, _x, sprintf, isRTL } from '@wordpress/i18n';
import { getFilename } from '@wordpress/url';
import { getBlockBindingsSource, switchToBlockType } from '@wordpress/blocks';
import { crop, overlayText, upload, chevronDown } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
import { store as coreStore } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { unlock } from '../lock-unlock';
import { createUpgradedEmbedBlock } from '../embed/util';
import { isExternalImage } from './edit';
import { Caption } from '../utils/caption';
import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
import { MIN_SIZE, ALLOWED_MEDIA_TYPES, SIZED_LAYOUTS, DEFAULT_MEDIA_SIZE_SLUG } from './constants';
import { evalAspectRatio } from './utils';
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
const {
DimensionsTool,
ResolutionTool
} = unlock(blockEditorPrivateApis);
const scaleOptions = [{
value: 'cover',
label: _x('Cover', 'Scale option for dimensions control'),
help: __('Image covers the space evenly.')
}, {
value: 'contain',
label: _x('Contain', 'Scale option for dimensions control'),
help: __('Image is contained without distortion.')
}];
const WRITEMODE_POPOVER_PROPS = {
placement: 'bottom-start'
};
// If the image has a href, wrap in an <a /> tag to trigger any inherited link element styles.
const ImageWrapper = ({
href,
children
}) => {
if (!href) {
return children;
}
return /*#__PURE__*/_jsx("a", {
href: href,
onClick: event => event.preventDefault(),
"aria-disabled": true,
style: {
// When the Image block is linked,
// it's wrapped with a disabled <a /> tag.
// Restore cursor style so it doesn't appear 'clickable'
// and remove pointer events. Safari needs the display property.
pointerEvents: 'none',
cursor: 'default',
display: 'inline'
},
children: children
});
};
function ContentOnlyControls({
attributes,
setAttributes,
lockAltControls,
lockAltControlsMessage,
lockTitleControls,
lockTitleControlsMessage
}) {
// Use internal state instead of a ref to make sure that the component
// re-renders when the popover's anchor updates.
const [popoverAnchor, setPopoverAnchor] = useState(null);
const [isAltDialogOpen, setIsAltDialogOpen] = useState(false);
const [isTitleDialogOpen, setIsTitleDialogOpen] = useState(false);
return /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx(ToolbarItem, {
ref: setPopoverAnchor,
children: toggleProps => /*#__PURE__*/_jsx(DropdownMenu, {
icon: chevronDown
/* translators: button label text should, if possible, be under 16 characters. */,
label: __('More'),
toggleProps: {
...toggleProps,
description: __('Displays more controls.')
},
popoverProps: WRITEMODE_POPOVER_PROPS,
children: ({
onClose
}) => /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx(MenuItem, {
onClick: () => {
setIsAltDialogOpen(true);
onClose();
},
"aria-haspopup": "dialog",
children: _x('Alternative text', 'Alternative text for an image. Block toolbar label, a low character count is preferred.')
}), /*#__PURE__*/_jsx(MenuItem, {
onClick: () => {
setIsTitleDialogOpen(true);
onClose();
},
"aria-haspopup": "dialog",
children: __('Title text')
})]
})
})
}), isAltDialogOpen && /*#__PURE__*/_jsx(Popover, {
placement: "bottom-start",
anchor: popoverAnchor,
onClose: () => setIsAltDialogOpen(false),
offset: 13,
variant: "toolbar",
children: /*#__PURE__*/_jsx("div", {
className: "wp-block-image__toolbar_content_textarea__container",
children: /*#__PURE__*/_jsx(TextareaControl, {
className: "wp-block-image__toolbar_content_textarea",
label: __('Alternative text'),
value: attributes.alt || '',
onChange: value => setAttributes({
alt: value
}),
disabled: lockAltControls,
help: lockAltControls ? /*#__PURE__*/_jsx(_Fragment, {
children: lockAltControlsMessage
}) : /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx(ExternalLink, {
href:
// translators: Localized tutorial, if one exists. W3C Web Accessibility Initiative link has list of existing translations.
__('https://www.w3.org/WAI/tutorials/images/decision-tree/'),
children: __('Describe the purpose of the image.')
}), /*#__PURE__*/_jsx("br", {}), __('Leave empty if decorative.')]
}),
__nextHasNoMarginBottom: true
})
})
}), isTitleDialogOpen && /*#__PURE__*/_jsx(Popover, {
placement: "bottom-start",
anchor: popoverAnchor,
onClose: () => setIsTitleDialogOpen(false),
offset: 13,
variant: "toolbar",
children: /*#__PURE__*/_jsx("div", {
className: "wp-block-image__toolbar_content_textarea__container",
children: /*#__PURE__*/_jsx(TextControl, {
__next40pxDefaultSize: true,
className: "wp-block-image__toolbar_content_textarea",
__nextHasNoMarginBottom: true,
label: __('Title attribute'),
value: attributes.title || '',
onChange: value => setAttributes({
title: value
}),
disabled: lockTitleControls,
help: lockTitleControls ? /*#__PURE__*/_jsx(_Fragment, {
children: lockTitleControlsMessage
}) : /*#__PURE__*/_jsxs(_Fragment, {
children: [__('Describe the role of this image on the page.'), /*#__PURE__*/_jsx(ExternalLink, {
href: "https://www.w3.org/TR/html52/dom.html#the-title-attribute",
children: __('(Note: many devices and browsers do not display this text.)')
})]
})
})
})
})]
});
}
export default function Image({
temporaryURL,
attributes,
setAttributes,
isSingleSelected,
insertBlocksAfter,
onReplace,
onSelectImage,
onSelectURL,
onUploadError,
context,
clientId,
blockEditingMode,
parentLayoutType,
maxContentWidth
}) {
const {
url = '',
alt,
align,
id,
href,
rel,
linkClass,
linkDestination,
title,
width,
height,
aspectRatio,
scale,
linkTarget,
sizeSlug,
lightbox,
metadata
} = attributes;
const [imageElement, setImageElement] = useState();
const [resizeDelta, setResizeDelta] = useState(null);
const [pixelSize, setPixelSize] = useState({});
const [offsetTop, setOffsetTop] = useState(0);
const setResizeObserved = useResizeObserver(([entry]) => {
if (!resizeDelta) {
const [box] = entry.borderBoxSize;
setPixelSize({
width: box.inlineSize,
height: box.blockSize
});
}
// This is usually 0 unless the image height is less than the line-height.
setOffsetTop(entry.target.offsetTop);
});
const effectResizeableBoxPlacement = useCallback(() => {
var _imageElement$offsetT;
setOffsetTop((_imageElement$offsetT = imageElement?.offsetTop) !== null && _imageElement$offsetT !== void 0 ? _imageElement$offsetT : 0);
}, [imageElement]);
const setRefs = useMergeRefs([setImageElement, setResizeObserved]);
const {
allowResize = true
} = context;
const image = useSelect(select => id && isSingleSelected ? select(coreStore).getMedia(id, {
context: 'view'
}) : null, [id, isSingleSelected]);
const {
canInsertCover,
imageEditing,
imageSizes,
maxWidth
} = useSelect(select => {
const {
getBlockRootClientId,
canInsertBlockType,
getSettings
} = select(blockEditorStore);
const rootClientId = getBlockRootClientId(clientId);
const settings = getSettings();
return {
imageEditing: settings.imageEditing,
imageSizes: settings.imageSizes,
maxWidth: settings.maxWidth,
canInsertCover: canInsertBlockType('core/cover', rootClientId)
};
}, [clientId]);
const {
getBlock,
getSettings
} = useSelect(blockEditorStore);
const {
replaceBlocks,
toggleSelection
} = useDispatch(blockEditorStore);
const {
createErrorNotice,
createSuccessNotice
} = useDispatch(noticesStore);
const {
editEntityRecord
} = useDispatch(coreStore);
const isLargeViewport = useViewportMatch('medium');
const isWideAligned = ['wide', 'full'].includes(align);
const [{
loadedNaturalWidth,
loadedNaturalHeight
}, setLoadedNaturalSize] = useState({});
const [isEditingImage, setIsEditingImage] = useState(false);
const [externalBlob, setExternalBlob] = useState();
const [hasImageErrored, setHasImageErrored] = useState(false);
const hasNonContentControls = blockEditingMode === 'default';
const isContentOnlyMode = blockEditingMode === 'contentOnly';
const isResizable = allowResize && hasNonContentControls && !isWideAligned && isLargeViewport;
const imageSizeOptions = imageSizes.filter(({
slug
}) => image?.media_details?.sizes?.[slug]?.source_url).map(({
name,
slug
}) => ({
value: slug,
label: name
}));
// If an image is externally hosted, try to fetch the image data. This may
// fail if the image host doesn't allow CORS with the domain. If it works,
// we can enable a button in the toolbar to upload the image.
useEffect(() => {
if (!isExternalImage(id, url) || !isSingleSelected || !getSettings().mediaUpload) {
setExternalBlob();
return;
}
if (externalBlob) {
return;
}
window
// Avoid cache, which seems to help avoid CORS problems.
.fetch(url.includes('?') ? url : url + '?').then(response => response.blob()).then(blob => setExternalBlob(blob))
// Do nothing, cannot upload.
.catch(() => {});
}, [id, url, isSingleSelected, externalBlob, getSettings]);
// Get naturalWidth and naturalHeight from image, and fall back to loaded natural
// width and height. This resolves an issue in Safari where the loaded natural
// width and height is otherwise lost when switching between alignments.
// See: https://github.com/WordPress/gutenberg/pull/37210.
const {
naturalWidth,
naturalHeight
} = useMemo(() => {
return {
naturalWidth: imageElement?.naturalWidth || loadedNaturalWidth || undefined,
naturalHeight: imageElement?.naturalHeight || loadedNaturalHeight || undefined
};
}, [loadedNaturalWidth, loadedNaturalHeight, imageElement?.complete]);
function onImageError() {
setHasImageErrored(true);
// Check if there's an embed block that handles this URL, e.g., instagram URL.
// See: https://github.com/WordPress/gutenberg/pull/11472
const embedBlock = createUpgradedEmbedBlock({
attributes: {
url
}
});
if (undefined !== embedBlock) {
onReplace(embedBlock);
}
}
function onImageLoad(event) {
setHasImageErrored(false);
setLoadedNaturalSize({
loadedNaturalWidth: event.target?.naturalWidth,
loadedNaturalHeight: event.target?.naturalHeight
});
}
function onSetHref(props) {
setAttributes(props);
}
function onSetLightbox(enable) {
if (enable && !lightboxSetting?.enabled) {
setAttributes({
lightbox: {
enabled: true
}
});
} else if (!enable && lightboxSetting?.enabled) {
setAttributes({
lightbox: {
enabled: false
}
});
} else {
setAttributes({
lightbox: undefined
});
}
}
function resetLightbox() {
// When deleting a link from an image while lightbox settings
// are enabled by default, we should disable the lightbox,
// otherwise the resulting UX looks like a mistake.
// See https://github.com/WordPress/gutenberg/pull/59890/files#r1532286123.
if (lightboxSetting?.enabled && lightboxSetting?.allowEditing) {
setAttributes({
lightbox: {
enabled: false
}
});
} else {
setAttributes({
lightbox: undefined
});
}
}
function onSetTitle(value) {
// This is the HTML title attribute, separate from the media object
// title.
setAttributes({
title: value
});
}
function updateAlt(newAlt) {
setAttributes({
alt: newAlt
});
}
function updateImage(newSizeSlug) {
const newUrl = image?.media_details?.sizes?.[newSizeSlug]?.source_url;
if (!newUrl) {
return null;
}
setAttributes({
url: newUrl,
sizeSlug: newSizeSlug
});
}
function uploadExternal() {
const {
mediaUpload
} = getSettings();
if (!mediaUpload) {
return;
}
mediaUpload({
filesList: [externalBlob],
onFileChange([img]) {
onSelectImage(img);
if (isBlobURL(img.url)) {
return;
}
setExternalBlob();
createSuccessNotice(__('Image uploaded.'), {
type: 'snackbar'
});
},
allowedTypes: ALLOWED_MEDIA_TYPES,
onError(message) {
createErrorNotice(message, {
type: 'snackbar'
});
}
});
}
useEffect(() => {
if (!isSingleSelected) {
setIsEditingImage(false);
}
}, [isSingleSelected]);
const canEditImage = id && naturalWidth && naturalHeight && imageEditing;
const allowCrop = isSingleSelected && canEditImage && !isEditingImage && !isContentOnlyMode;
function switchToCover() {
replaceBlocks(clientId, switchToBlockType(getBlock(clientId), 'core/cover'));
}
// TODO: Can allow more units after figuring out how they should interact
// with the ResizableBox and ImageEditor components. Calculations later on
// for those components are currently assuming px units.
const dimensionsUnitsOptions = useCustomUnits({
availableUnits: ['px']
});
const [lightboxSetting] = useSettings('lightbox');
const showLightboxSetting =
// If a block-level override is set, we should give users the option to
// remove that override, even if the lightbox UI is disabled in the settings.
!!lightbox && lightbox?.enabled !== lightboxSetting?.enabled || lightboxSetting?.allowEditing;
const lightboxChecked = !!lightbox?.enabled || !lightbox && !!lightboxSetting?.enabled;
const dropdownMenuProps = useToolsPanelDropdownMenuProps();
const dimensionsControl = isResizable && (SIZED_LAYOUTS.includes(parentLayoutType) ? /*#__PURE__*/_jsx(DimensionsTool, {
value: {
aspectRatio
},
onChange: ({
aspectRatio: newAspectRatio
}) => {
setAttributes({
aspectRatio: newAspectRatio,
scale: 'cover'
});
},
defaultAspectRatio: "auto",
tools: ['aspectRatio']
}) : /*#__PURE__*/_jsx(DimensionsTool, {
value: {
width,
height,
scale,
aspectRatio
},
onChange: ({
width: newWidth,
height: newHeight,
scale: newScale,
aspectRatio: newAspectRatio
}) => {
// Rebuilding the object forces setting `undefined`
// for values that are removed since setAttributes
// doesn't do anything with keys that aren't set.
setAttributes({
// CSS includes `height: auto`, but we need
// `width: auto` to fix the aspect ratio when
// only height is set due to the width and
// height attributes set via the server.
width: !newWidth && newHeight ? 'auto' : newWidth,
height: newHeight,
scale: newScale,
aspectRatio: newAspectRatio
});
},
defaultScale: "cover",
defaultAspectRatio: "auto",
scaleOptions: scaleOptions,
unitsOptions: dimensionsUnitsOptions
}));
const resetAll = () => {
setAttributes({
alt: undefined,
width: undefined,
height: undefined,
scale: undefined,
aspectRatio: undefined,
lightbox: undefined
});
updateImage(DEFAULT_MEDIA_SIZE_SLUG);
};
const sizeControls = /*#__PURE__*/_jsx(InspectorControls, {
children: /*#__PURE__*/_jsx(ToolsPanel, {
label: __('Settings'),
resetAll: resetAll,
dropdownMenuProps: dropdownMenuProps,
children: dimensionsControl
})
});
const arePatternOverridesEnabled = metadata?.bindings?.__default?.source === 'core/pattern-overrides';
const {
lockUrlControls = false,
lockHrefControls = false,
lockAltControls = false,
lockAltControlsMessage,
lockTitleControls = false,
lockTitleControlsMessage,
lockCaption = false
} = useSelect(select => {
if (!isSingleSelected) {
return {};
}
const {
url: urlBinding,
alt: altBinding,
title: titleBinding
} = metadata?.bindings || {};
const hasParentPattern = !!context['pattern/overrides'];
const urlBindingSource = getBlockBindingsSource(urlBinding?.source);
const altBindingSource = getBlockBindingsSource(altBinding?.source);
const titleBindingSource = getBlockBindingsSource(titleBinding?.source);
return {
lockUrlControls: !!urlBinding && !urlBindingSource?.canUserEditValue?.({
select,
context,
args: urlBinding?.args
}),
lockHrefControls:
// Disable editing the link of the URL if the image is inside a pattern instance.
// This is a temporary solution until we support overriding the link on the frontend.
hasParentPattern || arePatternOverridesEnabled,
lockCaption:
// Disable editing the caption if the image is inside a pattern instance.
// This is a temporary solution until we support overriding the caption on the frontend.
hasParentPattern,
lockAltControls: !!altBinding && !altBindingSource?.canUserEditValue?.({
select,
context,
args: altBinding?.args
}),
lockAltControlsMessage: altBindingSource?.label ? sprintf(/* translators: %s: Label of the bindings source. */
__('Connected to %s'), altBindingSource.label) : __('Connected to dynamic data'),
lockTitleControls: !!titleBinding && !titleBindingSource?.canUserEditValue?.({
select,
context,
args: titleBinding?.args
}),
lockTitleControlsMessage: titleBindingSource?.label ? sprintf(/* translators: %s: Label of the bindings source. */
__('Connected to %s'), titleBindingSource.label) : __('Connected to dynamic data')
};
}, [arePatternOverridesEnabled, context, isSingleSelected, metadata?.bindings]);
const showUrlInput = isSingleSelected && !isEditingImage && !lockHrefControls && !lockUrlControls;
const showCoverControls = isSingleSelected && canInsertCover;
const showBlockControls = showUrlInput || allowCrop || showCoverControls;
const mediaReplaceFlow = isSingleSelected && !isEditingImage && !lockUrlControls &&
/*#__PURE__*/
// For contentOnly mode, put this button in its own area so it has borders around it.
_jsx(BlockControls, {
group: isContentOnlyMode ? 'inline' : 'other',
children: /*#__PURE__*/_jsx(MediaReplaceFlow, {
mediaId: id,
mediaURL: url,
allowedTypes: ALLOWED_MEDIA_TYPES,
accept: "image/*",
onSelect: onSelectImage,
onSelectURL: onSelectURL,
onError: onUploadError,
name: !url ? __('Add image') : __('Replace'),
onReset: () => onSelectImage(undefined)
})
});
const controls = /*#__PURE__*/_jsxs(_Fragment, {
children: [showBlockControls && /*#__PURE__*/_jsxs(BlockControls, {
group: "block",
children: [showUrlInput && /*#__PURE__*/_jsx(ImageURLInputUI, {
url: href || '',
onChangeUrl: onSetHref,
linkDestination: linkDestination,
mediaUrl: image && image.source_url || url,
mediaLink: image && image.link,
linkTarget: linkTarget,
linkClass: linkClass,
rel: rel,
showLightboxSetting: showLightboxSetting,
lightboxEnabled: lightboxChecked,
onSetLightbox: onSetLightbox,
resetLightbox: resetLightbox
}), allowCrop && /*#__PURE__*/_jsx(ToolbarButton, {
onClick: () => setIsEditingImage(true),
icon: crop,
label: __('Crop')
}), showCoverControls && /*#__PURE__*/_jsx(ToolbarButton, {
icon: overlayText,
label: __('Add text over image'),
onClick: switchToCover
})]
}), isSingleSelected && externalBlob && /*#__PURE__*/_jsx(BlockControls, {
children: /*#__PURE__*/_jsx(ToolbarGroup, {
children: /*#__PURE__*/_jsx(ToolbarButton, {
onClick: uploadExternal,
icon: upload,
label: __('Upload to Media Library')
})
})
}), isContentOnlyMode &&
/*#__PURE__*/
// Add some extra controls for content attributes when content only mode is active.
// With content only mode active, the inspector is hidden, so users need another way
// to edit these attributes.
_jsx(BlockControls, {
group: "block",
children: /*#__PURE__*/_jsx(ContentOnlyControls, {
attributes: attributes,
setAttributes: setAttributes,
lockAltControls: lockAltControls,
lockAltControlsMessage: lockAltControlsMessage,
lockTitleControls: lockTitleControls,
lockTitleControlsMessage: lockTitleControlsMessage
})
}), /*#__PURE__*/_jsx(InspectorControls, {
children: /*#__PURE__*/_jsxs(ToolsPanel, {
label: __('Settings'),
resetAll: resetAll,
dropdownMenuProps: dropdownMenuProps,
children: [isSingleSelected && /*#__PURE__*/_jsx(ToolsPanelItem, {
label: __('Alternative text'),
isShownByDefault: true,
hasValue: () => !!alt,
onDeselect: () => setAttributes({
alt: undefined
}),
children: /*#__PURE__*/_jsx(TextareaControl, {
label: __('Alternative text'),
value: alt || '',
onChange: updateAlt,
readOnly: lockAltControls,
help: lockAltControls ? /*#__PURE__*/_jsx(_Fragment, {
children: lockAltControlsMessage
}) : /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx(ExternalLink, {
href:
// translators: Localized tutorial, if one exists. W3C Web Accessibility Initiative link has list of existing translations.
__('https://www.w3.org/WAI/tutorials/images/decision-tree/'),
children: __('Describe the purpose of the image.')
}), /*#__PURE__*/_jsx("br", {}), __('Leave empty if decorative.')]
}),
__nextHasNoMarginBottom: true
})
}), dimensionsControl, !!imageSizeOptions.length && /*#__PURE__*/_jsx(ResolutionTool, {
value: sizeSlug,
defaultValue: DEFAULT_MEDIA_SIZE_SLUG,
onChange: updateImage,
options: imageSizeOptions
})]
})
}), /*#__PURE__*/_jsx(InspectorControls, {
group: "advanced",
children: /*#__PURE__*/_jsx(TextControl, {
__nextHasNoMarginBottom: true,
__next40pxDefaultSize: true,
label: __('Title attribute'),
value: title || '',
onChange: onSetTitle,
readOnly: lockTitleControls,
help: lockTitleControls ? /*#__PURE__*/_jsx(_Fragment, {
children: lockTitleControlsMessage
}) : /*#__PURE__*/_jsxs(_Fragment, {
children: [__('Describe the role of this image on the page.'), /*#__PURE__*/_jsx(ExternalLink, {
href: "https://www.w3.org/TR/html52/dom.html#the-title-attribute",
children: __('(Note: many devices and browsers do not display this text.)')
})]
})
})
})]
});
const filename = getFilename(url);
let defaultedAlt;
if (alt) {
defaultedAlt = alt;
} else if (filename) {
defaultedAlt = sprintf(/* translators: %s: file name */
__('This image has an empty alt attribute; its file name is %s'), filename);
} else {
defaultedAlt = __('This image has an empty alt attribute');
}
const borderProps = useBorderProps(attributes);
const shadowProps = getShadowClassesAndStyles(attributes);
const isRounded = attributes.className?.includes('is-style-rounded');
const {
postType,
postId,
queryId
} = context;
const isDescendentOfQueryLoop = Number.isFinite(queryId);
let img = temporaryURL && hasImageErrored ?
/*#__PURE__*/
// Show a placeholder during upload when the blob URL can't be loaded. This can
// happen when the user uploads a HEIC image in a browser that doesn't support them.
_jsx(Placeholder, {
className: "wp-block-image__placeholder",
withIllustration: true,
children: /*#__PURE__*/_jsx(Spinner, {})
}) :
/*#__PURE__*/
// Disable reason: Image itself is not meant to be interactive, but
// should direct focus to block.
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx("img", {
src: temporaryURL || url,
alt: defaultedAlt,
onError: onImageError,
onLoad: onImageLoad,
ref: setRefs,
className: borderProps.className,
width: naturalWidth,
height: naturalHeight,
style: {
aspectRatio,
...(resizeDelta ? {
width: pixelSize.width + resizeDelta.width,
height: pixelSize.height + resizeDelta.height
} : {
width,
height
}),
objectFit: scale,
...borderProps.style,
...shadowProps.style
}
}), temporaryURL && /*#__PURE__*/_jsx(Spinner, {})]
})
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */;
if (canEditImage && isEditingImage) {
img = /*#__PURE__*/_jsx(ImageWrapper, {
href: href,
children: /*#__PURE__*/_jsx(ImageEditor, {
id: id,
url: url,
...pixelSize,
naturalHeight: naturalHeight,
naturalWidth: naturalWidth,
onSaveImage: imageAttributes => setAttributes(imageAttributes),
onFinishEditing: () => {
setIsEditingImage(false);
},
borderProps: isRounded ? undefined : borderProps
})
});
} else {
img = /*#__PURE__*/_jsx(ImageWrapper, {
href: href,
children: img
});
}
let resizableBox;
if (isResizable && isSingleSelected && !isEditingImage && !SIZED_LAYOUTS.includes(parentLayoutType)) {
const numericRatio = aspectRatio && evalAspectRatio(aspectRatio);
const customRatio = pixelSize.width / pixelSize.height;
const naturalRatio = naturalWidth / naturalHeight;
const ratio = numericRatio || customRatio || naturalRatio || 1;
const minWidth = naturalWidth < naturalHeight ? MIN_SIZE : MIN_SIZE * ratio;
const minHeight = naturalHeight < naturalWidth ? MIN_SIZE : MIN_SIZE / ratio;
// With the current implementation of ResizableBox, an image needs an
// explicit pixel value for the max-width. In absence of being able to
// set the content-width, this max-width is currently dictated by the
// vanilla editor style. The following variable adds a buffer to this
// vanilla style, so 3rd party themes have some wiggleroom. This does,
// in most cases, allow you to scale the image beyond the width of the
// main column, though not infinitely.
// @todo It would be good to revisit this once a content-width variable
// becomes available.
const maxWidthBuffer = maxWidth * 2.5;
const maxResizeWidth = maxContentWidth || maxWidthBuffer;
let showRightHandle = false;
let showLeftHandle = false;
/* eslint-disable no-lonely-if */
// See https://github.com/WordPress/gutenberg/issues/7584.
if (align === 'center') {
// When the image is centered, show both handles.
showRightHandle = true;
showLeftHandle = true;
} else if (isRTL()) {
// In RTL mode the image is on the right by default.
// Show the right handle and hide the left handle only when it is
// aligned left. Otherwise always show the left handle.
if (align === 'left') {
showRightHandle = true;
} else {
showLeftHandle = true;
}
} else {
// Show the left handle and hide the right handle only when the
// image is aligned right. Otherwise always show the right handle.
if (align === 'right') {
showLeftHandle = true;
} else {
showRightHandle = true;
}
}
/* eslint-enable no-lonely-if */
resizableBox = /*#__PURE__*/_jsx(ResizableBox, {
ref: effectResizeableBoxPlacement,
style: {
position: 'absolute',
// To match the vertical-align: bottom of the img (from style.scss)
// syncs the top with the img. This matters when the img height is
// less than the line-height.
inset: `${offsetTop}px 0 0 0`
},
size: pixelSize,
minWidth: minWidth,
maxWidth: maxResizeWidth,
minHeight: minHeight,
maxHeight: maxResizeWidth / ratio,
lockAspectRatio: ratio,
enable: {
top: false,
right: showRightHandle,
bottom: true,
left: showLeftHandle
},
onResizeStart: () => {
toggleSelection(false);
},
onResize: (event, direction, elt, delta) => {
setResizeDelta(delta);
},
onResizeStop: (event, direction, elt, delta) => {
toggleSelection(true);
setResizeDelta(null);
setPixelSize(current => ({
width: current.width + delta.width,
height: current.height + delta.height
}));
// Clear hardcoded width if the resized width is close to the max-content width.
if (maxContentWidth &&
// Only do this if the image is bigger than the container to prevent it from being squished.
// TODO: Remove this check if the image support setting 100% width.
naturalWidth >= maxContentWidth && Math.abs(elt.offsetWidth - maxContentWidth) < 10) {
setAttributes({
width: undefined,
height: undefined
});
return;
}
// Since the aspect ratio is locked when resizing, we can
// use the width of the resized element to calculate the
// height in CSS to prevent stretching when the max-width
// is reached.
setAttributes({
width: `${elt.offsetWidth}px`,
height: 'auto',
aspectRatio: ratio === naturalRatio ? undefined : String(ratio)
});
},
resizeRatio: align === 'center' ? 2 : 1
});
}
if (!url && !temporaryURL) {
return /*#__PURE__*/_jsxs(_Fragment, {
children: [mediaReplaceFlow, metadata?.bindings ? controls : sizeControls]
});
}
/**
* Set the post's featured image with the current image.
*/
const setPostFeatureImage = () => {
editEntityRecord('postType', postType, postId, {
featured_media: id
});
createSuccessNotice(__('Post featured image updated.'), {
type: 'snackbar'
});
};
const featuredImageControl = /*#__PURE__*/_jsx(BlockSettingsMenuControls, {
children: ({
selectedClientIds
}) => selectedClientIds.length === 1 && !isDescendentOfQueryLoop && postId && id && clientId === selectedClientIds[0] && /*#__PURE__*/_jsx(MenuItem, {
onClick: setPostFeatureImage,
children: __('Set as featured image')
})
});
return /*#__PURE__*/_jsxs(_Fragment, {
children: [mediaReplaceFlow, controls, featuredImageControl, img, resizableBox, /*#__PURE__*/_jsx(Caption, {
attributes: attributes,
setAttributes: setAttributes,
isSelected: isSingleSelected,
insertBlocksAfter: insertBlocksAfter,
label: __('Image caption text'),
showToolbarButton: isSingleSelected && hasNonContentControls && !arePatternOverridesEnabled,
readOnly: lockCaption
})]
});
}
//# sourceMappingURL=image.js.map