UNPKG

@wordpress/block-library

Version:
541 lines (480 loc) 18.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = Image; var _element = require("@wordpress/element"); var _lodash = require("lodash"); 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 _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 _util = require("../embed/util"); var _useClientWidth = _interopRequireDefault(require("./use-client-width")); var _edit = require("./edit"); var _constants = require("./constants"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ /** * Module constants */ function Image(_ref) { var _imageRef$current3, _attributes$className; let { temporaryURL, attributes, setAttributes, isSelected, insertBlocksAfter, onReplace, onSelectImage, onSelectURL, onUploadError, containerRef, context, clientId, isContentLocked } = _ref; const { url = '', alt, caption, align, id, href, rel, linkClass, linkDestination, title, width, height, linkTarget, sizeSlug } = attributes; const imageRef = (0, _element.useRef)(); const captionRef = (0, _element.useRef)(); const prevUrl = (0, _compose.usePrevious)(url); const { allowResize = true } = context; const { getBlock } = (0, _data.useSelect)(_blockEditor.store); const { image, multiImageSelection } = (0, _data.useSelect)(select => { const { getMedia } = select(_coreData.store); const { getMultiSelectedBlockClientIds, getBlockName } = select(_blockEditor.store); const multiSelectedClientIds = getMultiSelectedBlockClientIds(); return { image: id && isSelected ? getMedia(id, { context: 'view' }) : null, multiImageSelection: multiSelectedClientIds.length && multiSelectedClientIds.every(_clientId => getBlockName(_clientId) === 'core/image') }; }, [id, isSelected, clientId]); const { canInsertCover, imageEditing, imageSizes, maxWidth, mediaUpload } = (0, _data.useSelect)(select => { const { getBlockRootClientId, getSettings, canInsertBlockType } = select(_blockEditor.store); const rootClientId = getBlockRootClientId(clientId); const settings = (0, _lodash.pick)(getSettings(), ['imageEditing', 'imageSizes', 'maxWidth', 'mediaUpload']); return { ...settings, canInsertCover: canInsertBlockType('core/cover', rootClientId) }; }, [clientId]); const { replaceBlocks, toggleSelection } = (0, _data.useDispatch)(_blockEditor.store); const { createErrorNotice, createSuccessNotice } = (0, _data.useDispatch)(_notices.store); const isLargeViewport = (0, _compose.useViewportMatch)('medium'); const isWideAligned = (0, _lodash.includes)(['wide', 'full'], align); const [{ loadedNaturalWidth, loadedNaturalHeight }, setLoadedNaturalSize] = (0, _element.useState)({}); const [isEditingImage, setIsEditingImage] = (0, _element.useState)(false); const [externalBlob, setExternalBlob] = (0, _element.useState)(); const clientWidth = (0, _useClientWidth.default)(containerRef, [align]); const isResizable = allowResize && !isContentLocked && !(isWideAligned && isLargeViewport); const imageSizeOptions = (0, _lodash.map)((0, _lodash.filter)(imageSizes, _ref2 => { let { slug } = _ref2; return (0, _lodash.get)(image, ['media_details', 'sizes', slug, 'source_url']); }), _ref3 => { let { name, slug } = _ref3; return { 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) || !isSelected || externalBlob) { return; } window.fetch(url).then(response => response.blob()).then(blob => setExternalBlob(blob)) // Do nothing, cannot upload. .catch(() => {}); }, [id, url, isSelected, externalBlob]); // Focus the caption after inserting an image from the placeholder. This is // done to preserve the behaviour of focussing the first tabbable element // when a block is mounted. Previously, the image block would remount when // the placeholder is removed. Maybe this behaviour could be removed. (0, _element.useEffect)(() => { if (url && !prevUrl && isSelected) { captionRef.current.focus(); } }, [url, prevUrl]); // Get naturalWidth and naturalHeight from image ref, 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)(() => { var _imageRef$current, _imageRef$current2; return { naturalWidth: ((_imageRef$current = imageRef.current) === null || _imageRef$current === void 0 ? void 0 : _imageRef$current.naturalWidth) || loadedNaturalWidth || undefined, naturalHeight: ((_imageRef$current2 = imageRef.current) === null || _imageRef$current2 === void 0 ? void 0 : _imageRef$current2.naturalHeight) || loadedNaturalHeight || undefined }; }, [loadedNaturalWidth, loadedNaturalHeight, (_imageRef$current3 = imageRef.current) === null || _imageRef$current3 === void 0 ? void 0 : _imageRef$current3.complete]); function onResizeStart() { toggleSelection(false); } function onResizeStop() { toggleSelection(true); } function onImageError() { // 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 onSetHref(props) { setAttributes(props); } 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 = (0, _lodash.get)(image, ['media_details', 'sizes', newSizeSlug, 'source_url']); if (!newUrl) { return null; } setAttributes({ url: newUrl, width: undefined, height: undefined, sizeSlug: newSizeSlug }); } function uploadExternal() { mediaUpload({ filesList: [externalBlob], onFileChange(_ref4) { let [img] = _ref4; 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' }); } }); } function updateAlignment(nextAlign) { const extraUpdatedAttributes = ['wide', 'full'].includes(nextAlign) ? { width: undefined, height: undefined } : {}; setAttributes({ ...extraUpdatedAttributes, align: nextAlign }); } (0, _element.useEffect)(() => { if (!isSelected) { setIsEditingImage(false); } }, [isSelected]); const canEditImage = id && naturalWidth && naturalHeight && imageEditing; const allowCrop = !multiImageSelection && canEditImage && !isEditingImage; function switchToCover() { replaceBlocks(clientId, (0, _blocks.switchToBlockType)(getBlock(clientId), 'core/cover')); } const controls = (0, _element.createElement)(_element.Fragment, null, (0, _element.createElement)(_blockEditor.BlockControls, { group: "block" }, !isContentLocked && (0, _element.createElement)(_blockEditor.BlockAlignmentControl, { value: align, onChange: updateAlignment }), !multiImageSelection && !isEditingImage && (0, _element.createElement)(_blockEditor.__experimentalImageURLInputUI, { url: href || '', onChangeUrl: onSetHref, linkDestination: linkDestination, mediaUrl: image && image.source_url || url, mediaLink: image && image.link, linkTarget: linkTarget, linkClass: linkClass, rel: rel }), allowCrop && (0, _element.createElement)(_components.ToolbarButton, { onClick: () => setIsEditingImage(true), icon: _icons.crop, label: (0, _i18n.__)('Crop') }), externalBlob && (0, _element.createElement)(_components.ToolbarButton, { onClick: uploadExternal, icon: _icons.upload, label: (0, _i18n.__)('Upload external image') }), !multiImageSelection && canInsertCover && (0, _element.createElement)(_components.ToolbarButton, { icon: _icons.overlayText, label: (0, _i18n.__)('Add text over image'), onClick: switchToCover })), !multiImageSelection && !isEditingImage && (0, _element.createElement)(_blockEditor.BlockControls, { group: "other" }, (0, _element.createElement)(_blockEditor.MediaReplaceFlow, { mediaId: id, mediaURL: url, allowedTypes: _constants.ALLOWED_MEDIA_TYPES, accept: "image/*", onSelect: onSelectImage, onSelectURL: onSelectURL, onError: onUploadError })), (0, _element.createElement)(_blockEditor.InspectorControls, null, (0, _element.createElement)(_components.PanelBody, { title: (0, _i18n.__)('Settings') }, !multiImageSelection && (0, _element.createElement)(_components.TextareaControl, { label: (0, _i18n.__)('Alt text (alternative text)'), value: alt, onChange: updateAlt, help: (0, _element.createElement)(_element.Fragment, null, (0, _element.createElement)(_components.ExternalLink, { href: "https://www.w3.org/WAI/tutorials/images/decision-tree" }, (0, _i18n.__)('Describe the purpose of the image')), (0, _i18n.__)('Leave empty if the image is purely decorative.')) }), (0, _element.createElement)(_blockEditor.__experimentalImageSizeControl, { onChangeImage: updateImage, onChange: value => setAttributes(value), slug: sizeSlug, width: width, height: height, imageSizeOptions: imageSizeOptions, isResizable: isResizable, imageWidth: naturalWidth, imageHeight: naturalHeight }))), (0, _element.createElement)(_blockEditor.InspectorControls, { __experimentalGroup: "advanced" }, (0, _element.createElement)(_components.TextControl, { label: (0, _i18n.__)('Title attribute'), value: title || '', onChange: onSetTitle, help: (0, _element.createElement)(_element.Fragment, null, (0, _i18n.__)('Describe the role of this image on the page.'), (0, _element.createElement)(_components.ExternalLink, { href: "https://www.w3.org/TR/html52/dom.html#the-title-attribute" }, (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 isRounded = (_attributes$className = attributes.className) === null || _attributes$className === void 0 ? void 0 : _attributes$className.includes('is-style-rounded'); const hasCustomBorder = !!borderProps.className || !(0, _lodash.isEmpty)(borderProps.style); let img = // 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, _element.createElement)(_element.Fragment, null, (0, _element.createElement)("img", { src: temporaryURL || url, alt: defaultedAlt, onError: () => onImageError(), onLoad: event => { var _event$target, _event$target2; setLoadedNaturalSize({ loadedNaturalWidth: (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.naturalWidth, loadedNaturalHeight: (_event$target2 = event.target) === null || _event$target2 === void 0 ? void 0 : _event$target2.naturalHeight }); }, ref: imageRef, className: borderProps.className, style: borderProps.style }), temporaryURL && (0, _element.createElement)(_components.Spinner, null)) /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ ; let imageWidthWithinContainer; let imageHeightWithinContainer; if (clientWidth && naturalWidth && naturalHeight) { const exceedMaxWidth = naturalWidth > clientWidth; const ratio = naturalHeight / naturalWidth; imageWidthWithinContainer = exceedMaxWidth ? clientWidth : naturalWidth; imageHeightWithinContainer = exceedMaxWidth ? clientWidth * ratio : naturalHeight; } if (canEditImage && isEditingImage) { img = (0, _element.createElement)(_blockEditor.__experimentalImageEditor, { borderProps: isRounded ? undefined : borderProps, url: url, width: width, height: height, clientWidth: clientWidth, naturalHeight: naturalHeight, naturalWidth: naturalWidth }); } else if (!isResizable || !imageWidthWithinContainer) { img = (0, _element.createElement)("div", { style: { width, height } }, img); } else { const currentWidth = width || imageWidthWithinContainer; const currentHeight = height || imageHeightWithinContainer; const ratio = naturalWidth / naturalHeight; 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; 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 */ img = (0, _element.createElement)(_components.ResizableBox, { size: { width: width !== null && width !== void 0 ? width : 'auto', height: height && !hasCustomBorder ? height : 'auto' }, showHandle: isSelected, minWidth: minWidth, maxWidth: maxWidthBuffer, minHeight: minHeight, maxHeight: maxWidthBuffer / ratio, lockAspectRatio: true, enable: { top: false, right: showRightHandle, bottom: true, left: showLeftHandle }, onResizeStart: onResizeStart, onResizeStop: (event, direction, elt, delta) => { onResizeStop(); setAttributes({ width: parseInt(currentWidth + delta.width, 10), height: parseInt(currentHeight + delta.height, 10) }); } }, img); } return (0, _element.createElement)(_blockEditor.__experimentalImageEditingProvider, { id: id, url: url, naturalWidth: naturalWidth, naturalHeight: naturalHeight, clientWidth: clientWidth, onSaveImage: imageAttributes => setAttributes(imageAttributes), isEditing: isEditingImage, onFinishEditing: () => setIsEditingImage(false) }, !temporaryURL && controls, img, (!_blockEditor.RichText.isEmpty(caption) || isSelected) && (0, _element.createElement)(_blockEditor.RichText, { className: (0, _blockEditor.__experimentalGetElementClassName)('caption'), ref: captionRef, tagName: "figcaption", "aria-label": (0, _i18n.__)('Image caption text'), placeholder: (0, _i18n.__)('Add caption'), value: caption, onChange: value => setAttributes({ caption: value }), inlineToolbar: true, __unstableOnSplitAtEnd: () => insertBlocksAfter((0, _blocks.createBlock)((0, _blocks.getDefaultBlockName)())) })); } //# sourceMappingURL=image.js.map