UNPKG

@wordpress/block-library

Version:
496 lines (490 loc) • 18.8 kB
/** * External dependencies */ import clsx from 'clsx'; /** * WordPress dependencies */ import { BaseControl, PanelBody, SelectControl, ToggleControl, RangeControl, Spinner, MenuGroup, MenuItem, ToolbarDropdownMenu } from '@wordpress/components'; import { store as blockEditorStore, MediaPlaceholder, InspectorControls, useBlockProps, useInnerBlocksProps, BlockControls, MediaReplaceFlow, useSettings } from '@wordpress/block-editor'; import { Platform, useEffect, useMemo } from '@wordpress/element'; import { __, _x, sprintf } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; import { View } from '@wordpress/primitives'; import { createBlock } from '@wordpress/blocks'; import { createBlobURL } from '@wordpress/blob'; import { store as noticesStore } from '@wordpress/notices'; import { link as linkIcon, customLink, image as imageIcon, linkOff, fullscreen } from '@wordpress/icons'; /** * Internal dependencies */ import { sharedIcon } from './shared-icon'; import { defaultColumnsNumber, pickRelevantMediaFiles } from './shared'; import { getHrefAndDestination } from './utils'; import { getUpdatedLinkTargetSettings, getImageSizeAttributes } from '../image/utils'; import Gallery from './gallery'; import { LINK_DESTINATION_ATTACHMENT, LINK_DESTINATION_MEDIA, LINK_DESTINATION_NONE, LINK_DESTINATION_LIGHTBOX } from './constants'; import useImageSizes from './use-image-sizes'; import useGetNewImages from './use-get-new-images'; import useGetMedia from './use-get-media'; import GapStyles from './gap-styles'; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; const MAX_COLUMNS = 8; const LINK_OPTIONS = [{ icon: customLink, label: __('Link images to attachment pages'), value: LINK_DESTINATION_ATTACHMENT, noticeText: __('Attachment Pages') }, { icon: imageIcon, label: __('Link images to media files'), value: LINK_DESTINATION_MEDIA, noticeText: __('Media Files') }, { icon: fullscreen, label: __('Enlarge on click'), value: LINK_DESTINATION_LIGHTBOX, noticeText: __('Lightbox effect'), infoText: __('Scale images with a lightbox effect') }, { icon: linkOff, label: _x('None', 'Media item link option'), value: LINK_DESTINATION_NONE, noticeText: __('None') }]; const ALLOWED_MEDIA_TYPES = ['image']; const PLACEHOLDER_TEXT = Platform.isNative ? __('Add media') : __('Drag and drop images, upload, or choose from your library.'); const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.isNative ? { type: 'stepper' } : {}; const DEFAULT_BLOCK = { name: 'core/image' }; const EMPTY_ARRAY = []; export default function GalleryEdit(props) { const { setAttributes, attributes, className, clientId, isSelected, insertBlocksAfter, isContentLocked, onFocus } = props; const [lightboxSetting] = useSettings('blocks.core/image.lightbox'); const linkOptions = !lightboxSetting?.allowEditing ? LINK_OPTIONS.filter(option => option.value !== LINK_DESTINATION_LIGHTBOX) : LINK_OPTIONS; const { columns, imageCrop, randomOrder, linkTarget, linkTo, sizeSlug } = attributes; const { __unstableMarkNextChangeAsNotPersistent, replaceInnerBlocks, updateBlockAttributes, selectBlock } = useDispatch(blockEditorStore); const { createSuccessNotice, createErrorNotice } = useDispatch(noticesStore); const { getBlock, getSettings, innerBlockImages, blockWasJustInserted, multiGallerySelection } = useSelect(select => { var _getBlock$innerBlocks; const { getBlockName, getMultiSelectedBlockClientIds, getSettings: _getSettings, getBlock: _getBlock, wasBlockJustInserted } = select(blockEditorStore); const multiSelectedClientIds = getMultiSelectedBlockClientIds(); return { getBlock: _getBlock, getSettings: _getSettings, innerBlockImages: (_getBlock$innerBlocks = _getBlock(clientId)?.innerBlocks) !== null && _getBlock$innerBlocks !== void 0 ? _getBlock$innerBlocks : EMPTY_ARRAY, blockWasJustInserted: wasBlockJustInserted(clientId, 'inserter_menu'), multiGallerySelection: multiSelectedClientIds.length && multiSelectedClientIds.every(_clientId => getBlockName(_clientId) === 'core/gallery') }; }, [clientId]); const images = useMemo(() => innerBlockImages?.map(block => ({ clientId: block.clientId, id: block.attributes.id, url: block.attributes.url, attributes: block.attributes, fromSavedContent: Boolean(block.originalContent) })), [innerBlockImages]); const imageData = useGetMedia(innerBlockImages); const newImages = useGetNewImages(images, imageData); useEffect(() => { newImages?.forEach(newImage => { // Update the images data without creating new undo levels. __unstableMarkNextChangeAsNotPersistent(); updateBlockAttributes(newImage.clientId, { ...buildImageAttributes(newImage.attributes), id: newImage.id, align: undefined }); }); }, [newImages]); const imageSizeOptions = useImageSizes(imageData, isSelected, getSettings); /** * Determines the image attributes that should be applied to an image block * after the gallery updates. * * The gallery will receive the full collection of images when a new image * is added. As a result we need to reapply the image's original settings if * it already existed in the gallery. If the image is in fact new, we need * to apply the gallery's current settings to the image. * * @param {Object} imageAttributes Media object for the actual image. * @return {Object} Attributes to set on the new image block. */ function buildImageAttributes(imageAttributes) { const image = imageAttributes.id ? imageData.find(({ id }) => id === imageAttributes.id) : null; let newClassName; if (imageAttributes.className && imageAttributes.className !== '') { newClassName = imageAttributes.className; } let newLinkTarget; if (imageAttributes.linkTarget || imageAttributes.rel) { // When transformed from image blocks, the link destination and rel attributes are inherited. newLinkTarget = { linkTarget: imageAttributes.linkTarget, rel: imageAttributes.rel }; } else { // When an image is added, update the link destination and rel attributes according to the gallery settings newLinkTarget = getUpdatedLinkTargetSettings(linkTarget, attributes); } return { ...pickRelevantMediaFiles(image, sizeSlug), ...getHrefAndDestination(image, linkTo, imageAttributes?.linkDestination), ...newLinkTarget, className: newClassName, sizeSlug, caption: imageAttributes.caption || image.caption?.raw, alt: imageAttributes.alt || image.alt_text }; } function isValidFileType(file) { // It's necessary to retrieve the media type from the raw image data for already-uploaded images on native. const nativeFileData = Platform.isNative && file.id ? imageData.find(({ id }) => id === file.id) : null; const mediaTypeSelector = nativeFileData ? nativeFileData?.media_type : file.type; return ALLOWED_MEDIA_TYPES.some(mediaType => mediaTypeSelector?.indexOf(mediaType) === 0) || file.blob; } function updateImages(selectedImages) { const newFileUploads = Object.prototype.toString.call(selectedImages) === '[object FileList]'; const imageArray = newFileUploads ? Array.from(selectedImages).map(file => { if (!file.url) { return { blob: createBlobURL(file) }; } return file; }) : selectedImages; if (!imageArray.every(isValidFileType)) { createErrorNotice(__('If uploading to a gallery all files need to be image formats'), { id: 'gallery-upload-invalid-file', type: 'snackbar' }); } const processedImages = imageArray.filter(file => file.url || isValidFileType(file)).map(file => { if (!file.url) { return { blob: file.blob || createBlobURL(file) }; } return file; }); // Because we are reusing existing innerImage blocks any reordering // done in the media library will be lost so we need to reapply that ordering // once the new image blocks are merged in with existing. const newOrderMap = processedImages.reduce((result, image, index) => (result[image.id] = index, result), {}); const existingImageBlocks = !newFileUploads ? innerBlockImages.filter(block => processedImages.find(img => img.id === block.attributes.id)) : innerBlockImages; const newImageList = processedImages.filter(img => !existingImageBlocks.find(existingImg => img.id === existingImg.attributes.id)); const newBlocks = newImageList.map(image => { return createBlock('core/image', { id: image.id, blob: image.blob, url: image.url, caption: image.caption, alt: image.alt }); }); replaceInnerBlocks(clientId, existingImageBlocks.concat(newBlocks).sort((a, b) => newOrderMap[a.attributes.id] - newOrderMap[b.attributes.id])); // Select the first block to scroll into view when new blocks are added. if (newBlocks?.length > 0) { selectBlock(newBlocks[0].clientId); } } function onUploadError(message) { createErrorNotice(message, { type: 'snackbar' }); } function setLinkTo(value) { setAttributes({ linkTo: value }); const changedAttributes = {}; const blocks = []; getBlock(clientId).innerBlocks.forEach(block => { blocks.push(block.clientId); const image = block.attributes.id ? imageData.find(({ id }) => id === block.attributes.id) : null; changedAttributes[block.clientId] = getHrefAndDestination(image, value, false, block.attributes, lightboxSetting); }); updateBlockAttributes(blocks, changedAttributes, true); const linkToText = [...linkOptions].find(linkType => linkType.value === value); createSuccessNotice(sprintf(/* translators: %s: image size settings */ __('All gallery image links updated to: %s'), linkToText.noticeText), { id: 'gallery-attributes-linkTo', type: 'snackbar' }); } function setColumnsNumber(value) { setAttributes({ columns: value }); } function toggleImageCrop() { setAttributes({ imageCrop: !imageCrop }); } function toggleRandomOrder() { setAttributes({ randomOrder: !randomOrder }); } function toggleOpenInNewTab(openInNewTab) { const newLinkTarget = openInNewTab ? '_blank' : undefined; setAttributes({ linkTarget: newLinkTarget }); const changedAttributes = {}; const blocks = []; getBlock(clientId).innerBlocks.forEach(block => { blocks.push(block.clientId); changedAttributes[block.clientId] = getUpdatedLinkTargetSettings(newLinkTarget, block.attributes); }); updateBlockAttributes(blocks, changedAttributes, true); const noticeText = openInNewTab ? __('All gallery images updated to open in new tab') : __('All gallery images updated to not open in new tab'); createSuccessNotice(noticeText, { id: 'gallery-attributes-openInNewTab', type: 'snackbar' }); } function updateImagesSize(newSizeSlug) { setAttributes({ sizeSlug: newSizeSlug }); const changedAttributes = {}; const blocks = []; getBlock(clientId).innerBlocks.forEach(block => { blocks.push(block.clientId); const image = block.attributes.id ? imageData.find(({ id }) => id === block.attributes.id) : null; changedAttributes[block.clientId] = getImageSizeAttributes(image, newSizeSlug); }); updateBlockAttributes(blocks, changedAttributes, true); const imageSize = imageSizeOptions.find(size => size.value === newSizeSlug); createSuccessNotice(sprintf(/* translators: %s: image size settings */ __('All gallery image sizes updated to: %s'), imageSize.label), { id: 'gallery-attributes-sizeSlug', type: 'snackbar' }); } useEffect(() => { // linkTo attribute must be saved so blocks don't break when changing image_default_link_type in options.php. if (!linkTo) { __unstableMarkNextChangeAsNotPersistent(); setAttributes({ linkTo: window?.wp?.media?.view?.settings?.defaultProps?.link || LINK_DESTINATION_NONE }); } }, [linkTo]); const hasImages = !!images.length; const hasImageIds = hasImages && images.some(image => !!image.id); const imagesUploading = images.some(img => !Platform.isNative ? !img.id && img.url?.indexOf('blob:') === 0 : img.url?.indexOf('file:') === 0); // MediaPlaceholder props are different between web and native hence, we provide a platform-specific set. const mediaPlaceholderProps = Platform.select({ web: { addToGallery: false, disableMediaButtons: imagesUploading, value: {} }, native: { addToGallery: hasImageIds, isAppender: hasImages, disableMediaButtons: hasImages && !isSelected || imagesUploading, value: hasImageIds ? images : {}, autoOpenMediaUpload: !hasImages && isSelected && blockWasJustInserted, onFocus } }); const mediaPlaceholder = /*#__PURE__*/_jsx(MediaPlaceholder, { handleUpload: false, icon: sharedIcon, labels: { title: __('Gallery'), instructions: PLACEHOLDER_TEXT }, onSelect: updateImages, accept: "image/*", allowedTypes: ALLOWED_MEDIA_TYPES, multiple: true, onError: onUploadError, ...mediaPlaceholderProps }); const blockProps = useBlockProps({ className: clsx(className, 'has-nested-images') }); const nativeInnerBlockProps = Platform.isNative && { marginHorizontal: 0, marginVertical: 0 }; const innerBlocksProps = useInnerBlocksProps(blockProps, { defaultBlock: DEFAULT_BLOCK, directInsert: true, orientation: 'horizontal', renderAppender: false, ...nativeInnerBlockProps }); if (!hasImages) { return /*#__PURE__*/_jsxs(View, { ...innerBlocksProps, children: [innerBlocksProps.children, mediaPlaceholder] }); } const hasLinkTo = linkTo && linkTo !== 'none'; return /*#__PURE__*/_jsxs(_Fragment, { children: [/*#__PURE__*/_jsx(InspectorControls, { children: /*#__PURE__*/_jsxs(PanelBody, { title: __('Settings'), children: [images.length > 1 && /*#__PURE__*/_jsx(RangeControl, { __nextHasNoMarginBottom: true, label: __('Columns'), value: columns ? columns : defaultColumnsNumber(images.length), onChange: setColumnsNumber, min: 1, max: Math.min(MAX_COLUMNS, images.length), ...MOBILE_CONTROL_PROPS_RANGE_CONTROL, required: true, __next40pxDefaultSize: true }), imageSizeOptions?.length > 0 && /*#__PURE__*/_jsx(SelectControl, { __nextHasNoMarginBottom: true, label: __('Resolution'), help: __('Select the size of the source images.'), value: sizeSlug, options: imageSizeOptions, onChange: updateImagesSize, hideCancelButton: true, size: "__unstable-large" }), Platform.isNative ? /*#__PURE__*/_jsx(SelectControl, { __nextHasNoMarginBottom: true, label: __('Link'), value: linkTo, onChange: setLinkTo, options: linkOptions, hideCancelButton: true, size: "__unstable-large" }) : null, /*#__PURE__*/_jsx(ToggleControl, { __nextHasNoMarginBottom: true, label: __('Crop images to fit'), checked: !!imageCrop, onChange: toggleImageCrop }), /*#__PURE__*/_jsx(ToggleControl, { __nextHasNoMarginBottom: true, label: __('Randomize order'), checked: !!randomOrder, onChange: toggleRandomOrder }), hasLinkTo && /*#__PURE__*/_jsx(ToggleControl, { __nextHasNoMarginBottom: true, label: __('Open images in new tab'), checked: linkTarget === '_blank', onChange: toggleOpenInNewTab }), Platform.isWeb && !imageSizeOptions && hasImageIds && /*#__PURE__*/_jsxs(BaseControl, { className: "gallery-image-sizes", __nextHasNoMarginBottom: true, children: [/*#__PURE__*/_jsx(BaseControl.VisualLabel, { children: __('Resolution') }), /*#__PURE__*/_jsxs(View, { className: "gallery-image-sizes__loading", children: [/*#__PURE__*/_jsx(Spinner, {}), __('Loading options…')] })] })] }) }), Platform.isWeb ? /*#__PURE__*/_jsx(BlockControls, { group: "block", children: /*#__PURE__*/_jsx(ToolbarDropdownMenu, { icon: linkIcon, label: __('Link'), children: ({ onClose }) => /*#__PURE__*/_jsx(MenuGroup, { children: linkOptions.map(linkItem => { const isOptionSelected = linkTo === linkItem.value; return /*#__PURE__*/_jsx(MenuItem, { isSelected: isOptionSelected, className: clsx('components-dropdown-menu__menu-item', { 'is-active': isOptionSelected }), iconPosition: "left", icon: linkItem.icon, onClick: () => { setLinkTo(linkItem.value); onClose(); }, role: "menuitemradio", info: linkItem.infoText, children: linkItem.label }, linkItem.value); }) }) }) }) : null, Platform.isWeb && /*#__PURE__*/_jsxs(_Fragment, { children: [!multiGallerySelection && /*#__PURE__*/_jsx(BlockControls, { group: "other", children: /*#__PURE__*/_jsx(MediaReplaceFlow, { allowedTypes: ALLOWED_MEDIA_TYPES, accept: "image/*", handleUpload: false, onSelect: updateImages, name: __('Add'), multiple: true, mediaIds: images.filter(image => image.id).map(image => image.id), addToGallery: hasImageIds }) }), /*#__PURE__*/_jsx(GapStyles, { blockGap: attributes.style?.spacing?.blockGap, clientId: clientId })] }), /*#__PURE__*/_jsx(Gallery, { ...props, isContentLocked: isContentLocked, images: images, mediaPlaceholder: !hasImages || Platform.isNative ? mediaPlaceholder : undefined, blockProps: innerBlocksProps, insertBlocksAfter: insertBlocksAfter, multiGallerySelection: multiGallerySelection })] }); } //# sourceMappingURL=edit.js.map