UNPKG

@wordpress/block-library

Version:
475 lines (457 loc) 16.8 kB
/** * WordPress dependencies */ import { useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { decodeEntities } from '@wordpress/html-entities'; import { __ } from '@wordpress/i18n'; import { cloneBlock, getBlockSupport, store as blocksStore } from '@wordpress/blocks'; /** @typedef {import('@wordpress/blocks').WPBlockVariation} WPBlockVariation */ /** @typedef {import('@wordpress/components/build-types/query-controls/types').OrderByOption} OrderByOption */ /** * @typedef IHasNameAndId * @property {string|number} id The entity's id. * @property {string} name The entity's name. */ /** * The object used in Query block that contains info and helper mappings * from an array of IHasNameAndId objects. * * @typedef {Object} QueryEntitiesInfo * @property {IHasNameAndId[]} entities The array of entities. * @property {Object<string, IHasNameAndId>} mapById Object mapping with the id as key and the entity as value. * @property {Object<string, IHasNameAndId>} mapByName Object mapping with the name as key and the entity as value. * @property {string[]} names Array with the entities' names. */ /** * Returns a helper object with mapping from Objects that implement * the `IHasNameAndId` interface. The returned object is used for * integration with `FormTokenField` component. * * @param {IHasNameAndId[]} entities The entities to extract of helper object. * @return {QueryEntitiesInfo} The object with the entities information. */ export const getEntitiesInfo = entities => { const mapping = entities?.reduce((accumulator, entity) => { const { mapById, mapByName, names } = accumulator; mapById[entity.id] = entity; mapByName[entity.name] = entity; names.push(entity.name); return accumulator; }, { mapById: {}, mapByName: {}, names: [] }); return { entities, ...mapping }; }; /** * Helper util to return a value from a certain path of the object. * Path is specified as a string of properties, separated by dots, * for example: "parent.child". * * @param {Object} object Input object. * @param {string} path Path to the object property. * @return {*} Value of the object property at the specified path. */ export const getValueFromObjectPath = (object, path) => { const normalizedPath = path.split('.'); let value = object; normalizedPath.forEach(fieldName => { value = value?.[fieldName]; }); return value; }; /** * Helper util to map records to add a `name` prop from a * provided path, in order to handle all entities in the same * fashion(implementing`IHasNameAndId` interface). * * @param {Object[]} entities The array of entities. * @param {string} path The path to map a `name` property from the entity. * @return {IHasNameAndId[]} An array of entities that now implement the `IHasNameAndId` interface. */ export const mapToIHasNameAndId = (entities, path) => { return (entities || []).map(entity => ({ ...entity, name: decodeEntities(getValueFromObjectPath(entity, path)) })); }; /** * Returns a helper object that contains: * 1. An `options` object from the available post types, to be passed to a `SelectControl`. * 2. A helper map with available taxonomies per post type. * 3. A helper map with post format support per post type. * * @return {Object} The helper object related to post types. */ export const usePostTypes = () => { const postTypes = useSelect(select => { const { getPostTypes } = select(coreStore); const excludedPostTypes = ['attachment']; const filteredPostTypes = getPostTypes({ per_page: -1 })?.filter(({ viewable, slug }) => viewable && !excludedPostTypes.includes(slug)); return filteredPostTypes; }, []); const postTypesTaxonomiesMap = useMemo(() => { if (!postTypes?.length) { return; } return postTypes.reduce((accumulator, type) => { accumulator[type.slug] = type.taxonomies; return accumulator; }, {}); }, [postTypes]); const postTypesSelectOptions = useMemo(() => (postTypes || []).map(({ labels, slug }) => ({ label: labels.singular_name, value: slug })), [postTypes]); const postTypeFormatSupportMap = useMemo(() => { if (!postTypes?.length) { return {}; } return postTypes.reduce((accumulator, type) => { accumulator[type.slug] = type.supports?.['post-formats'] || false; return accumulator; }, {}); }, [postTypes]); return { postTypesTaxonomiesMap, postTypesSelectOptions, postTypeFormatSupportMap }; }; /** * Hook that returns the taxonomies associated with a specific post type. * * @param {string} postType The post type from which to retrieve the associated taxonomies. * @return {Object[]} An array of the associated taxonomies. */ export const useTaxonomies = postType => { const taxonomies = useSelect(select => { const { getTaxonomies, getPostType } = select(coreStore); // Does the post type have taxonomies? if (getPostType(postType)?.taxonomies?.length > 0) { return getTaxonomies({ type: postType, per_page: -1 }); } return []; }, [postType]); return useMemo(() => { return taxonomies?.filter(({ visibility }) => !!visibility?.publicly_queryable); }, [taxonomies]); }; /** * Hook that returns whether a specific post type is hierarchical. * * @param {string} postType The post type to check. * @return {boolean} Whether a specific post type is hierarchical. */ export function useIsPostTypeHierarchical(postType) { return useSelect(select => { const type = select(coreStore).getPostType(postType); return type?.viewable && type?.hierarchical; }, [postType]); } /** * List of avaiable options to order by. * * @param {string} postType The post type to check. * @return {OrderByOption[]} List of order options. */ export function useOrderByOptions(postType) { const supportsCustomOrder = useSelect(select => { const type = select(coreStore).getPostType(postType); return !!type?.supports?.['page-attributes']; }, [postType]); return useMemo(() => { const orderByOptions = [{ label: __('Newest to oldest'), value: 'date/desc' }, { label: __('Oldest to newest'), value: 'date/asc' }, { /* translators: Label for ordering posts by title in ascending order. */ label: __('A → Z'), value: 'title/asc' }, { /* translators: Label for ordering posts by title in descending order. */ label: __('Z → A'), value: 'title/desc' }]; if (supportsCustomOrder) { orderByOptions.push({ /* translators: Label for ordering posts by ascending menu order. */ label: __('Ascending by order'), value: 'menu_order/asc' }, { /* translators: Label for ordering posts by descending menu order. */ label: __('Descending by order'), value: 'menu_order/desc' }); } return orderByOptions; }, [supportsCustomOrder]); } /** * Hook that returns the query properties' names defined by the active * block variation, to determine which block's filters to show. * * @param {Object} attributes Block attributes. * @return {string[]} An array of the query attributes. */ export function useAllowedControls(attributes) { return useSelect(select => select(blocksStore).getActiveBlockVariation('core/query', attributes)?.allowedControls, [attributes]); } export function isControlAllowed(allowedControls, key) { // Every controls is allowed if the list is not defined. if (!allowedControls) { return true; } return allowedControls.includes(key); } /** * Clones a pattern's blocks and then recurses over that list of blocks, * transforming them to retain some `query` attribute properties. * For now we retain the `postType` and `inherit` properties as they are * fundamental for the expected functionality of the block and don't affect * its design and presentation. * * Returns the cloned/transformed blocks and array of existing Query Loop * client ids for further manipulation, in order to avoid multiple recursions. * * @param {WPBlock[]} blocks The list of blocks to look through and transform(mutate). * @param {Record<string,*>} queryBlockAttributes The existing Query Loop's attributes. * @return {{ newBlocks: WPBlock[], queryClientIds: string[] }} An object with the cloned/transformed blocks and all the Query Loop clients from these blocks. */ export const getTransformedBlocksFromPattern = (blocks, queryBlockAttributes) => { const { query: { postType, inherit }, namespace } = queryBlockAttributes; const clonedBlocks = blocks.map(block => cloneBlock(block)); const queryClientIds = []; const blocksQueue = [...clonedBlocks]; while (blocksQueue.length > 0) { const block = blocksQueue.shift(); if (block.name === 'core/query') { block.attributes.query = { ...block.attributes.query, postType, inherit }; if (namespace) { block.attributes.namespace = namespace; } queryClientIds.push(block.clientId); } block.innerBlocks?.forEach(innerBlock => { blocksQueue.push(innerBlock); }); } return { newBlocks: clonedBlocks, queryClientIds }; }; /** * Helper hook that determines if there is an active variation of the block * and if there are available specific patterns for this variation. * If there are, these patterns are going to be the only ones suggested to * the user in setup and replace flow, without including the default ones * for Query Loop. * * If there are no such patterns, the default ones for Query Loop are going * to be suggested. * * @param {string} clientId The block's client ID. * @param {Object} attributes The block's attributes. * @return {string} The block name to be used in the patterns suggestions. */ export function useBlockNameForPatterns(clientId, attributes) { return useSelect(select => { const activeVariationName = select(blocksStore).getActiveBlockVariation('core/query', attributes)?.name; if (!activeVariationName) { return 'core/query'; } const { getBlockRootClientId, getPatternsByBlockTypes } = select(blockEditorStore); const rootClientId = getBlockRootClientId(clientId); const activePatterns = getPatternsByBlockTypes(`core/query/${activeVariationName}`, rootClientId); return activePatterns.length > 0 ? `core/query/${activeVariationName}` : 'core/query'; }, [clientId, attributes]); } /** * Helper hook that determines if there is an active variation of the block * and if there are available specific scoped `block` variations connected with * this variation. * * If there are, these variations are going to be the only ones suggested * to the user in setup flow when clicking to `start blank`, without including * the default ones for Query Loop. * * If there are no such scoped `block` variations, the default ones for Query * Loop are going to be suggested. * * The way we determine such variations is with the convention that they have the `namespace` * attribute defined as an array. This array should contain the names(`name` property) of any * variations they want to be connected to. * For example, if we have a `Query Loop` scoped `inserter` variation with the name `products`, * we can connect a scoped `block` variation by setting its `namespace` attribute to `['products']`. * If the user selects this variation, the `namespace` attribute will be overridden by the * main `inserter` variation. * * @param {Object} attributes The block's attributes. * @return {WPBlockVariation[]} The block variations to be suggested in setup flow, when clicking to `start blank`. */ export function useScopedBlockVariations(attributes) { const { activeVariationName, blockVariations } = useSelect(select => { const { getActiveBlockVariation, getBlockVariations } = select(blocksStore); return { activeVariationName: getActiveBlockVariation('core/query', attributes)?.name, blockVariations: getBlockVariations('core/query', 'block') }; }, [attributes]); const variations = useMemo(() => { // Filter out the variations that have defined a `namespace` attribute, // which means they are 'connected' to specific variations of the block. const isNotConnected = variation => !variation.attributes?.namespace; if (!activeVariationName) { return blockVariations.filter(isNotConnected); } const connectedVariations = blockVariations.filter(variation => variation.attributes?.namespace?.includes(activeVariationName)); if (!!connectedVariations.length) { return connectedVariations; } return blockVariations.filter(isNotConnected); }, [activeVariationName, blockVariations]); return variations; } /** * Hook that returns the block patterns for a specific block type. * * @param {string} clientId The block's client ID. * @param {string} name The block type name. * @return {Object[]} An array of valid block patterns. */ export const usePatterns = (clientId, name) => { return useSelect(select => { const { getBlockRootClientId, getPatternsByBlockTypes } = select(blockEditorStore); const rootClientId = getBlockRootClientId(clientId); return getPatternsByBlockTypes(name, rootClientId); }, [name, clientId]); }; /** * The object returned by useUnsupportedBlocks with info about the type of * unsupported blocks present inside the Query block. * * @typedef {Object} UnsupportedBlocksInfo * @property {boolean} hasBlocksFromPlugins True if blocks from plugins are present. * @property {boolean} hasPostContentBlock True if a 'core/post-content' block is present. * @property {boolean} hasUnsupportedBlocks True if there are any unsupported blocks. */ /** * Hook that returns an object with information about the unsupported blocks * present inside a Query Loop with the given `clientId`. The returned object * contains props that are true when a certain type of unsupported block is * present. * * @param {string} clientId The block's client ID. * @return {UnsupportedBlocksInfo} The object containing the information. */ export const useUnsupportedBlocks = clientId => { return useSelect(select => { const { getClientIdsOfDescendants, getBlockName } = select(blockEditorStore); const blocks = {}; getClientIdsOfDescendants(clientId).forEach(descendantClientId => { const blockName = getBlockName(descendantClientId); /* * Client side navigation can be true in two states: * - supports.interactivity = true; * - supports.interactivity.clientNavigation = true; */ const blockSupportsInteractivity = Object.is(getBlockSupport(blockName, 'interactivity'), true); const blockSupportsInteractivityClientNavigation = getBlockSupport(blockName, 'interactivity.clientNavigation'); const blockInteractivity = blockSupportsInteractivity || blockSupportsInteractivityClientNavigation; if (!blockInteractivity) { blocks.hasBlocksFromPlugins = true; } else if (blockName === 'core/post-content') { blocks.hasPostContentBlock = true; } }); blocks.hasUnsupportedBlocks = blocks.hasBlocksFromPlugins || blocks.hasPostContentBlock; return blocks; }, [clientId]); }; /** * Helper function that returns the query context from the editor based on the * available template slug. * * @param {string} templateSlug Current template slug based on context. * @return {Object} An object with isSingular and templateType properties. */ export function getQueryContextFromTemplate(templateSlug) { // In the Post Editor, the template slug is not available. if (!templateSlug) { return { isSingular: true }; } let isSingular = false; let templateType = templateSlug === 'wp' ? 'custom' : templateSlug; const singularTemplates = ['404', 'blank', 'single', 'page', 'custom']; const templateTypeFromSlug = templateSlug.includes('-') ? templateSlug.split('-', 1)[0] : templateSlug; const queryFromTemplateSlug = templateSlug.includes('-') ? templateSlug.split('-').slice(1).join('-') : ''; if (queryFromTemplateSlug) { templateType = templateTypeFromSlug; } isSingular = singularTemplates.includes(templateType); return { isSingular, templateType }; } //# sourceMappingURL=utils.js.map