UNPKG

@wordpress/block-library

Version:
284 lines (263 loc) 10.8 kB
import { useMemo } from '@wordpress/element'; import { useDispatch, useSelect, useRegistry } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { createBlock } from '@wordpress/blocks'; import { pickRelevantMediaFiles } from './shared'; import { getHrefAndDestination } from './utils'; import { getUpdatedLinkTargetSettings } from '../image/utils'; import { getSourceQuery, getDynamicSource, ATTACHED_MEDIA, DEFAULT_ORDERBY, DEFAULT_ORDER, MAX_IMAGES, } from './dynamic-source'; const EMPTY_ARRAY = []; /** * Builds the attributes for a `core/image` block from a media (attachment) * record, applying the gallery-wide settings that affect how the image renders. * * Used to construct the (non-persisted) image blocks previewed in dynamic mode, * and the real image blocks created when a dynamic gallery is converted * ("pinned") back to individual images. The frontend equivalent is * `block_core_gallery_render_dynamic_image()` in `index.php`. * * @param {Object} media A media object as returned by the REST API. * @param {Object} galleryAttributes The gallery block's attributes. * @return {Object} Attributes to pass to `createBlock( 'core/image', ... )`. */ function buildImageBlockAttributes( media, galleryAttributes ) { const { sizeSlug, linkTo, linkTarget, aspectRatio } = galleryAttributes; const hasAspectRatio = !! aspectRatio && aspectRatio !== 'auto'; return { id: media.id, ...pickRelevantMediaFiles( media, sizeSlug ), ...getHrefAndDestination( media, linkTo ), ...getUpdatedLinkTargetSettings( linkTarget, galleryAttributes ), sizeSlug, // Raw caption, mirroring the frontend (`index.php`). Gap: the REST API // exposes no caption run through `wp_get_attachment_caption`, so neither // side applies that filter. caption: media.caption?.raw || '', alt: media.alt_text || '', aspectRatio: hasAspectRatio ? aspectRatio : undefined, // Pair `scale` with `aspectRatio` so the image crops rather than stretches, // matching the image block's UI and the frontend (`index.php`). scale: hasAspectRatio ? 'cover' : undefined, }; } /** * Builds a set of `core/image` blocks from the resolved media, applying the * gallery-wide settings. Each call mints fresh client IDs, so it can produce * both the editor preview and the materialized inner blocks on convert. * * @param {Object[]} media Media records from the REST API. * @param {Object} galleryAttributes The image-relevant gallery attributes. * @return {Object[]} New `core/image` block instances. */ function buildImageBlocks( media, galleryAttributes ) { return media.map( ( mediaItem ) => createBlock( 'core/image', buildImageBlockAttributes( mediaItem, galleryAttributes ) ) ); } /** * Bundles the Gallery block's "dynamic mode" source resolution and actions. * * Dynamic mode resolves the gallery's images from a configured source * (`attributes.dynamicContent`) instead of from manually-added inner image * blocks. This hook centralizes the shared, single-instance pieces — the source * resolution (one `getEntityRecords`), the editor-preview blocks, and the * mode/ordering actions — out of the block's `edit` component. Transient UI * concerns (e.g. the convert-to-dynamic confirmation) live in the components * that own them. * * @param {Object} options * @param {Object} options.attributes The gallery block attributes. * @param {Function} options.setAttributes The block's `setAttributes`. * @param {string} options.clientId The block client ID. * @param {?number} options.postId The current post ID (from block context). * @param {?string} options.postType The current post type (from block context). * @return {Object} Dynamic-mode source data and actions. */ export default function useDynamicGallery( { attributes, setAttributes, clientId, postId, postType, } ) { const { dynamicContent } = attributes; // Whether dynamic mode makes sense in the current editing context. A // `postType` means the block will resolve against some post at render time — // either a concrete post (post/page editor, Query Loop item) or a post-bound // template (`single`, `page`) whose post is filled in by `get_the_ID()` on the // frontend (see `index.php`). Without it (template part, pattern, generic // template) there's no post to attach to, so the source can never resolve. const canUseDynamicSource = !! postType; // The descriptor for the configured source (its `title`/`description`/ // `emptyMessage`), resolved once here so consumers read the copy without // re-deriving it from `dynamicContent`. `undefined` for an unknown source. const sourceDescriptor = getDynamicSource( dynamicContent?.source ); // Current source ordering, falling back to the shared defaults when unset. const sourceOrderby = dynamicContent?.args?.orderBy ?? DEFAULT_ORDERBY; const sourceOrder = dynamicContent?.args?.order ?? DEFAULT_ORDER; const registry = useRegistry(); const { replaceInnerBlocks, __unstableMarkNextChangeAsNotPersistent } = useDispatch( blockEditorStore ); // Resolve the configured source to a media query. `null` (static mode, or an // unresolvable source) short-circuits the select below so no request fires. const query = useMemo( () => dynamicContent ? getSourceQuery( dynamicContent, { postId } ) : null, [ dynamicContent, postId ] ); const { dynamicMedia, dynamicMediaTotal, isResolvingDynamic } = useSelect( ( select ) => { if ( ! query ) { return { dynamicMedia: EMPTY_ARRAY, dynamicMediaTotal: 0, isResolvingDynamic: false, }; } const selectorArgs = [ 'postType', 'attachment', query ]; return { dynamicMedia: select( coreStore ).getEntityRecords( ...selectorArgs ) ?? EMPTY_ARRAY, // Total matching attachments (the `X-WP-Total` header), which the // query's `per_page` cap doesn't bound — so it reveals when the // post has more attached images than are shown. dynamicMediaTotal: select( coreStore ).getEntityRecordsTotalItems( ...selectorArgs ) ?? 0, isResolvingDynamic: ! select( coreStore ).hasFinishedResolution( 'getEntityRecords', selectorArgs ), }; }, [ query ] ); // The source caps results at `MAX_IMAGES` (matching the frontend), so flag // when the post has more attached images than the gallery can show. const hasMoreImagesThanCap = dynamicMediaTotal > MAX_IMAGES; // The only gallery settings that affect how an image renders, and so the // only ones `buildImageBlockAttributes` reads. Depending on this narrowed // set (rather than the whole `attributes` object) keeps the preview from // rebuilding on unrelated edits, e.g. typing in the gallery caption. const { sizeSlug, linkTo, linkTarget, aspectRatio } = attributes; const imageAttributes = useMemo( () => ( { sizeSlug, linkTo, linkTarget, aspectRatio } ), [ sizeSlug, linkTo, linkTarget, aspectRatio ] ); // The (non-persisted) `core/image` blocks used for the editor preview. // Rebuilt when the resolved media or an image-relevant setting changes. const dynamicImageBlocks = useMemo( () => buildImageBlocks( dynamicMedia, imageAttributes ), [ dynamicMedia, imageAttributes ] ); // Context the gallery provides to its (previewed) image blocks. const galleryContext = useMemo( () => ( { allowResize: attributes.allowResize ?? false, imageCrop: attributes.imageCrop, fixedHeight: attributes.fixedHeight, navigationButtonType: attributes.navigationButtonType, } ), [ attributes.allowResize, attributes.imageCrop, attributes.fixedHeight, attributes.navigationButtonType, ] ); // Switches the gallery into dynamic mode, displaying images attached to the // current post. Clearing the inner blocks removes the manually-added images: // they're the gallery's image data, so there's nothing else to reset. The // legacy `images`/`ids` attributes aren't touched — they're back-compat shims // for the pre-innerBlocks format (see `deprecated.js`/`transforms.js`), empty // on any gallery reachable here. function enableDynamicMode() { // Batch the attribute change and the inner-block reset into a single // undo level: they're two halves of one mode switch, so one undo should // revert both. Marking the second dispatch non-persistent stops it from // opening a second undo level, which would otherwise leave the gallery // in a half-switched state (dynamic source set, images still present). registry.batch( () => { setAttributes( { dynamicContent: { source: ATTACHED_MEDIA } } ); __unstableMarkNextChangeAsNotPersistent(); replaceInnerBlocks( clientId, [] ); } ); } // "Pins" a dynamic gallery: materializes the currently-resolved media as // real, editable image blocks and leaves dynamic mode. function convertToStatic() { // Batch the inner-block materialization and the attribute change into a // single undo level so one undo reverts the whole conversion (see // `enableDynamicMode`). Build fresh blocks rather than reusing the // preview's `dynamicImageBlocks` so the materialized inner blocks get // their own client IDs, distinct from the (disabled) preview instances. registry.batch( () => { replaceInnerBlocks( clientId, buildImageBlocks( dynamicMedia, imageAttributes ) ); __unstableMarkNextChangeAsNotPersistent(); setAttributes( { dynamicContent: undefined } ); } ); } // Updates the source ordering within `dynamicContent.args`. Passing // `undefined` (or the default order) strips the keys so they aren't // persisted redundantly and the ToolsPanel item reads as unset. function setSourceOrder( nextOrderby, nextOrder ) { const nextArgs = { ...dynamicContent?.args }; delete nextArgs.orderBy; delete nextArgs.order; if ( nextOrderby !== undefined && ( nextOrderby !== DEFAULT_ORDERBY || nextOrder !== DEFAULT_ORDER ) ) { nextArgs.orderBy = nextOrderby; nextArgs.order = nextOrder; } const nextSource = { ...dynamicContent }; if ( Object.keys( nextArgs ).length ) { nextSource.args = nextArgs; } else { delete nextSource.args; } setAttributes( { dynamicContent: nextSource } ); } // Resets the source to its bare form: keeps the source kind, drops its args. function resetSource() { setAttributes( { dynamicContent: { source: dynamicContent.source }, } ); } return { dynamicContent, canUseDynamicSource, sourceDescriptor, hasMoreImagesThanCap, dynamicMediaTotal, sourceOrderby, sourceOrder, dynamicMedia, dynamicImageBlocks, isResolvingDynamic, galleryContext, enableDynamicMode, convertToStatic, setSourceOrder, resetSource, }; }