@wordpress/block-library
Version:
Block library for the WordPress editor.
541 lines (480 loc) • 18.2 kB
JavaScript
"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