UNPKG

@wordpress/block-library

Version:
929 lines (916 loc) 35.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = Image; var _blob = require("@wordpress/blob"); var _components = require("@wordpress/components"); var _compose = require("@wordpress/compose"); var _data = require("@wordpress/data"); var _blockEditor = require("@wordpress/block-editor"); var _element = require("@wordpress/element"); var _i18n = require("@wordpress/i18n"); var _url = require("@wordpress/url"); var _blocks = require("@wordpress/blocks"); var _icons = require("@wordpress/icons"); var _notices = require("@wordpress/notices"); var _coreData = require("@wordpress/core-data"); var _lockUnlock = require("../lock-unlock"); var _util = require("../embed/util"); var _edit = require("./edit"); var _caption = require("../utils/caption"); var _hooks = require("../utils/hooks"); var _constants = require("./constants"); var _utils = require("./utils"); var _jsxRuntime = require("react/jsx-runtime"); /** * WordPress dependencies */ /** * Internal dependencies */ const { DimensionsTool, ResolutionTool } = (0, _lockUnlock.unlock)(_blockEditor.privateApis); const scaleOptions = [{ value: 'cover', label: (0, _i18n._x)('Cover', 'Scale option for dimensions control'), help: (0, _i18n.__)('Image covers the space evenly.') }, { value: 'contain', label: (0, _i18n._x)('Contain', 'Scale option for dimensions control'), help: (0, _i18n.__)('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__*/(0, _jsxRuntime.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] = (0, _element.useState)(null); const [isAltDialogOpen, setIsAltDialogOpen] = (0, _element.useState)(false); const [isTitleDialogOpen, setIsTitleDialogOpen] = (0, _element.useState)(false); return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ToolbarItem, { ref: setPopoverAnchor, children: toggleProps => /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.DropdownMenu, { icon: _icons.chevronDown /* translators: button label text should, if possible, be under 16 characters. */, label: (0, _i18n.__)('More'), toggleProps: { ...toggleProps, description: (0, _i18n.__)('Displays more controls.') }, popoverProps: WRITEMODE_POPOVER_PROPS, children: ({ onClose }) => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.MenuItem, { onClick: () => { setIsAltDialogOpen(true); onClose(); }, "aria-haspopup": "dialog", children: (0, _i18n._x)('Alternative text', 'Alternative text for an image. Block toolbar label, a low character count is preferred.') }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.MenuItem, { onClick: () => { setIsTitleDialogOpen(true); onClose(); }, "aria-haspopup": "dialog", children: (0, _i18n.__)('Title text') })] }) }) }), isAltDialogOpen && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Popover, { placement: "bottom-start", anchor: popoverAnchor, onClose: () => setIsAltDialogOpen(false), offset: 13, variant: "toolbar", children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: "wp-block-image__toolbar_content_textarea__container", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextareaControl, { className: "wp-block-image__toolbar_content_textarea", label: (0, _i18n.__)('Alternative text'), value: attributes.alt || '', onChange: value => setAttributes({ alt: value }), disabled: lockAltControls, help: lockAltControls ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, { children: lockAltControlsMessage }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ExternalLink, { href: // translators: Localized tutorial, if one exists. W3C Web Accessibility Initiative link has list of existing translations. (0, _i18n.__)('https://www.w3.org/WAI/tutorials/images/decision-tree/'), children: (0, _i18n.__)('Describe the purpose of the image.') }), /*#__PURE__*/(0, _jsxRuntime.jsx)("br", {}), (0, _i18n.__)('Leave empty if decorative.')] }), __nextHasNoMarginBottom: true }) }) }), isTitleDialogOpen && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Popover, { placement: "bottom-start", anchor: popoverAnchor, onClose: () => setIsTitleDialogOpen(false), offset: 13, variant: "toolbar", children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: "wp-block-image__toolbar_content_textarea__container", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextControl, { __next40pxDefaultSize: true, className: "wp-block-image__toolbar_content_textarea", __nextHasNoMarginBottom: true, label: (0, _i18n.__)('Title attribute'), value: attributes.title || '', onChange: value => setAttributes({ title: value }), disabled: lockTitleControls, help: lockTitleControls ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, { children: lockTitleControlsMessage }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [(0, _i18n.__)('Describe the role of this image on the page.'), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ExternalLink, { href: "https://www.w3.org/TR/html52/dom.html#the-title-attribute", children: (0, _i18n.__)('(Note: many devices and browsers do not display this text.)') })] }) }) }) })] }); } 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] = (0, _element.useState)(); const [resizeDelta, setResizeDelta] = (0, _element.useState)(null); const [pixelSize, setPixelSize] = (0, _element.useState)({}); const [offsetTop, setOffsetTop] = (0, _element.useState)(0); const setResizeObserved = (0, _compose.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 = (0, _element.useCallback)(() => { var _imageElement$offsetT; setOffsetTop((_imageElement$offsetT = imageElement?.offsetTop) !== null && _imageElement$offsetT !== void 0 ? _imageElement$offsetT : 0); }, [imageElement]); const setRefs = (0, _compose.useMergeRefs)([setImageElement, setResizeObserved]); const { allowResize = true } = context; const image = (0, _data.useSelect)(select => id && isSingleSelected ? select(_coreData.store).getMedia(id, { context: 'view' }) : null, [id, isSingleSelected]); const { canInsertCover, imageEditing, imageSizes, maxWidth } = (0, _data.useSelect)(select => { const { getBlockRootClientId, canInsertBlockType, getSettings } = select(_blockEditor.store); 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 } = (0, _data.useSelect)(_blockEditor.store); const { replaceBlocks, toggleSelection } = (0, _data.useDispatch)(_blockEditor.store); const { createErrorNotice, createSuccessNotice } = (0, _data.useDispatch)(_notices.store); const { editEntityRecord } = (0, _data.useDispatch)(_coreData.store); const isLargeViewport = (0, _compose.useViewportMatch)('medium'); const isWideAligned = ['wide', 'full'].includes(align); const [{ loadedNaturalWidth, loadedNaturalHeight }, setLoadedNaturalSize] = (0, _element.useState)({}); const [isEditingImage, setIsEditingImage] = (0, _element.useState)(false); const [externalBlob, setExternalBlob] = (0, _element.useState)(); const [hasImageErrored, setHasImageErrored] = (0, _element.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. (0, _element.useEffect)(() => { if (!(0, _edit.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 } = (0, _element.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 = (0, _util.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 ((0, _blob.isBlobURL)(img.url)) { return; } setExternalBlob(); createSuccessNotice((0, _i18n.__)('Image uploaded.'), { type: 'snackbar' }); }, allowedTypes: _constants.ALLOWED_MEDIA_TYPES, onError(message) { createErrorNotice(message, { type: 'snackbar' }); } }); } (0, _element.useEffect)(() => { if (!isSingleSelected) { setIsEditingImage(false); } }, [isSingleSelected]); const canEditImage = id && naturalWidth && naturalHeight && imageEditing; const allowCrop = isSingleSelected && canEditImage && !isEditingImage && !isContentOnlyMode; function switchToCover() { replaceBlocks(clientId, (0, _blocks.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 = (0, _components.__experimentalUseCustomUnits)({ availableUnits: ['px'] }); const [lightboxSetting] = (0, _blockEditor.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 = (0, _hooks.useToolsPanelDropdownMenuProps)(); const dimensionsControl = isResizable && (_constants.SIZED_LAYOUTS.includes(parentLayoutType) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(DimensionsTool, { value: { aspectRatio }, onChange: ({ aspectRatio: newAspectRatio }) => { setAttributes({ aspectRatio: newAspectRatio, scale: 'cover' }); }, defaultAspectRatio: "auto", tools: ['aspectRatio'] }) : /*#__PURE__*/(0, _jsxRuntime.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(_constants.DEFAULT_MEDIA_SIZE_SLUG); }; const sizeControls = /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.InspectorControls, { children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToolsPanel, { label: (0, _i18n.__)('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 } = (0, _data.useSelect)(select => { if (!isSingleSelected) { return {}; } const { url: urlBinding, alt: altBinding, title: titleBinding } = metadata?.bindings || {}; const hasParentPattern = !!context['pattern/overrides']; const urlBindingSource = (0, _blocks.getBlockBindingsSource)(urlBinding?.source); const altBindingSource = (0, _blocks.getBlockBindingsSource)(altBinding?.source); const titleBindingSource = (0, _blocks.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 ? (0, _i18n.sprintf)(/* translators: %s: Label of the bindings source. */ (0, _i18n.__)('Connected to %s'), altBindingSource.label) : (0, _i18n.__)('Connected to dynamic data'), lockTitleControls: !!titleBinding && !titleBindingSource?.canUserEditValue?.({ select, context, args: titleBinding?.args }), lockTitleControlsMessage: titleBindingSource?.label ? (0, _i18n.sprintf)(/* translators: %s: Label of the bindings source. */ (0, _i18n.__)('Connected to %s'), titleBindingSource.label) : (0, _i18n.__)('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. (0, _jsxRuntime.jsx)(_blockEditor.BlockControls, { group: isContentOnlyMode ? 'inline' : 'other', children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.MediaReplaceFlow, { mediaId: id, mediaURL: url, allowedTypes: _constants.ALLOWED_MEDIA_TYPES, accept: "image/*", onSelect: onSelectImage, onSelectURL: onSelectURL, onError: onUploadError, name: !url ? (0, _i18n.__)('Add image') : (0, _i18n.__)('Replace'), onReset: () => onSelectImage(undefined) }) }); const controls = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [showBlockControls && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_blockEditor.BlockControls, { group: "block", children: [showUrlInput && /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.__experimentalImageURLInputUI, { 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__*/(0, _jsxRuntime.jsx)(_components.ToolbarButton, { onClick: () => setIsEditingImage(true), icon: _icons.crop, label: (0, _i18n.__)('Crop') }), showCoverControls && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ToolbarButton, { icon: _icons.overlayText, label: (0, _i18n.__)('Add text over image'), onClick: switchToCover })] }), isSingleSelected && externalBlob && /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.BlockControls, { children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ToolbarGroup, { children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ToolbarButton, { onClick: uploadExternal, icon: _icons.upload, label: (0, _i18n.__)('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. (0, _jsxRuntime.jsx)(_blockEditor.BlockControls, { group: "block", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(ContentOnlyControls, { attributes: attributes, setAttributes: setAttributes, lockAltControls: lockAltControls, lockAltControlsMessage: lockAltControlsMessage, lockTitleControls: lockTitleControls, lockTitleControlsMessage: lockTitleControlsMessage }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.InspectorControls, { children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_components.__experimentalToolsPanel, { label: (0, _i18n.__)('Settings'), resetAll: resetAll, dropdownMenuProps: dropdownMenuProps, children: [isSingleSelected && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalToolsPanelItem, { label: (0, _i18n.__)('Alternative text'), isShownByDefault: true, hasValue: () => !!alt, onDeselect: () => setAttributes({ alt: undefined }), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextareaControl, { label: (0, _i18n.__)('Alternative text'), value: alt || '', onChange: updateAlt, readOnly: lockAltControls, help: lockAltControls ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, { children: lockAltControlsMessage }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ExternalLink, { href: // translators: Localized tutorial, if one exists. W3C Web Accessibility Initiative link has list of existing translations. (0, _i18n.__)('https://www.w3.org/WAI/tutorials/images/decision-tree/'), children: (0, _i18n.__)('Describe the purpose of the image.') }), /*#__PURE__*/(0, _jsxRuntime.jsx)("br", {}), (0, _i18n.__)('Leave empty if decorative.')] }), __nextHasNoMarginBottom: true }) }), dimensionsControl, !!imageSizeOptions.length && /*#__PURE__*/(0, _jsxRuntime.jsx)(ResolutionTool, { value: sizeSlug, defaultValue: _constants.DEFAULT_MEDIA_SIZE_SLUG, onChange: updateImage, options: imageSizeOptions })] }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.InspectorControls, { group: "advanced", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.TextControl, { __nextHasNoMarginBottom: true, __next40pxDefaultSize: true, label: (0, _i18n.__)('Title attribute'), value: title || '', onChange: onSetTitle, readOnly: lockTitleControls, help: lockTitleControls ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, { children: lockTitleControlsMessage }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [(0, _i18n.__)('Describe the role of this image on the page.'), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.ExternalLink, { href: "https://www.w3.org/TR/html52/dom.html#the-title-attribute", children: (0, _i18n.__)('(Note: many devices and browsers do not display this text.)') })] }) }) })] }); const filename = (0, _url.getFilename)(url); let defaultedAlt; if (alt) { defaultedAlt = alt; } else if (filename) { defaultedAlt = (0, _i18n.sprintf)(/* translators: %s: file name */ (0, _i18n.__)('This image has an empty alt attribute; its file name is %s'), filename); } else { defaultedAlt = (0, _i18n.__)('This image has an empty alt attribute'); } const borderProps = (0, _blockEditor.__experimentalUseBorderProps)(attributes); const shadowProps = (0, _blockEditor.__experimentalGetShadowClassesAndStyles)(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. (0, _jsxRuntime.jsx)(_components.Placeholder, { className: "wp-block-image__placeholder", withIllustration: true, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.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 */ (0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.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__*/(0, _jsxRuntime.jsx)(_components.Spinner, {})] }) /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */; if (canEditImage && isEditingImage) { img = /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageWrapper, { href: href, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.__experimentalImageEditor, { id: id, url: url, ...pixelSize, naturalHeight: naturalHeight, naturalWidth: naturalWidth, onSaveImage: imageAttributes => setAttributes(imageAttributes), onFinishEditing: () => { setIsEditingImage(false); }, borderProps: isRounded ? undefined : borderProps }) }); } else { img = /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageWrapper, { href: href, children: img }); } let resizableBox; if (isResizable && isSingleSelected && !isEditingImage && !_constants.SIZED_LAYOUTS.includes(parentLayoutType)) { const numericRatio = aspectRatio && (0, _utils.evalAspectRatio)(aspectRatio); const customRatio = pixelSize.width / pixelSize.height; const naturalRatio = naturalWidth / naturalHeight; const ratio = numericRatio || customRatio || naturalRatio || 1; const minWidth = naturalWidth < naturalHeight ? _constants.MIN_SIZE : _constants.MIN_SIZE * ratio; const minHeight = naturalHeight < naturalWidth ? _constants.MIN_SIZE : _constants.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 ((0, _i18n.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__*/(0, _jsxRuntime.jsx)(_components.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__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.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((0, _i18n.__)('Post featured image updated.'), { type: 'snackbar' }); }; const featuredImageControl = /*#__PURE__*/(0, _jsxRuntime.jsx)(_blockEditor.BlockSettingsMenuControls, { children: ({ selectedClientIds }) => selectedClientIds.length === 1 && !isDescendentOfQueryLoop && postId && id && clientId === selectedClientIds[0] && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.MenuItem, { onClick: setPostFeatureImage, children: (0, _i18n.__)('Set as featured image') }) }); return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [mediaReplaceFlow, controls, featuredImageControl, img, resizableBox, /*#__PURE__*/(0, _jsxRuntime.jsx)(_caption.Caption, { attributes: attributes, setAttributes: setAttributes, isSelected: isSingleSelected, insertBlocksAfter: insertBlocksAfter, label: (0, _i18n.__)('Image caption text'), showToolbarButton: isSingleSelected && hasNonContentControls && !arePatternOverridesEnabled, readOnly: lockCaption })] }); } //# sourceMappingURL=image.js.map