UNPKG

@wordpress/block-library

Version:
225 lines (203 loc) 6.83 kB
/** * WordPress dependencies */ import { createBlock, parse } from '@wordpress/blocks'; import { applyFilters } from '@wordpress/hooks'; /** * Convert a flat menu item structure to a nested blocks structure. * * @param {Object[]} menuItems An array of menu items. * * @return {WPBlock[]} An array of blocks. */ export default function menuItemsToBlocks( menuItems ) { if ( ! menuItems ) { return null; } const menuTree = createDataTree( menuItems ); const blocks = mapMenuItemsToBlocks( menuTree ); return applyFilters( 'blocks.navigation.__unstableMenuItemsToBlocks', blocks, menuItems ); } /** * A recursive function that maps menu item nodes to blocks. * * @param {WPNavMenuItem[]} menuItems An array of WPNavMenuItem items. * @param {number} level An integer representing the nesting level. * @return {Object} Object containing innerBlocks and mapping. */ function mapMenuItemsToBlocks( menuItems, level = 0 ) { let mapping = {}; // The menuItem should be in menu_order sort order. const sortedItems = [ ...menuItems ].sort( ( a, b ) => a.menu_order - b.menu_order ); const innerBlocks = sortedItems.map( ( menuItem ) => { if ( menuItem.type === 'block' ) { const [ block ] = parse( menuItem.content.raw ); if ( ! block ) { return createBlock( 'core/freeform', { content: menuItem.content, } ); } return block; } const blockType = menuItem.children?.length ? 'core/navigation-submenu' : 'core/navigation-link'; const attributes = menuItemToBlockAttributes( menuItem, blockType, level ); // If there are children recurse to build those nested blocks. const { innerBlocks: nestedBlocks = [], // alias to avoid shadowing mapping: nestedMapping = {}, // alias to avoid shadowing } = menuItem.children?.length ? mapMenuItemsToBlocks( menuItem.children, level + 1 ) : {}; // Update parent mapping with nested mapping. mapping = { ...mapping, ...nestedMapping, }; // Create block with nested "innerBlocks". const block = createBlock( blockType, attributes, nestedBlocks ); // Create mapping for menuItem -> block. mapping[ menuItem.id ] = block.clientId; return block; } ); return { innerBlocks, mapping, }; } /** * A WP nav_menu_item object. * For more documentation on the individual fields present on a menu item please see: * https://core.trac.wordpress.org/browser/tags/5.7.1/src/wp-includes/nav-menu.php#L789 * * @typedef WPNavMenuItem * * @property {Object} title stores the raw and rendered versions of the title/label for this menu item. * @property {Array} xfn the XFN relationships expressed in the link of this menu item. * @property {Array} classes the HTML class attributes for this menu item. * @property {string} attr_title the HTML title attribute for this menu item. * @property {string} object The type of object originally represented, such as 'category', 'post', or 'attachment'. * @property {string} object_id The DB ID of the original object this menu item represents, e.g. ID for posts and term_id for categories. * @property {string} description The description of this menu item. * @property {string} url The URL to which this menu item points. * @property {string} type The family of objects originally represented, such as 'post_type' or 'taxonomy'. * @property {string} target The target attribute of the link element for this menu item. */ /** * Convert block attributes to menu item. * * @param {WPNavMenuItem} menuItem the menu item to be converted to block attributes. * @param {string} blockType The block type. * @param {number} level An integer representing the nesting level. * @return {Object} the block attributes converted from the WPNavMenuItem item. */ function menuItemToBlockAttributes( { title: menuItemTitleField, xfn, classes, // eslint-disable-next-line camelcase attr_title, object, // eslint-disable-next-line camelcase object_id, description, url, type: menuItemTypeField, target, }, blockType, level ) { // For historical reasons, the `core/navigation-link` variation type is `tag` // whereas WP Core expects `post_tag` as the `object` type. // To avoid writing a block migration we perform a conversion here. // See also inverse equivalent in `blockAttributesToMenuItem`. if ( object && object === 'post_tag' ) { object = 'tag'; } return { label: menuItemTitleField?.rendered || '', ...( object?.length && { type: object, } ), kind: menuItemTypeField?.replace( '_', '-' ) || 'custom', url: url || '', ...( xfn?.length && xfn.join( ' ' ).trim() && { rel: xfn.join( ' ' ).trim(), } ), ...( classes?.length && classes.join( ' ' ).trim() && { className: classes.join( ' ' ).trim(), } ), /* eslint-disable camelcase */ ...( attr_title?.length && { title: attr_title, } ), ...( object_id && 'custom' !== object && { id: object_id, } ), /* eslint-enable camelcase */ ...( description?.length && { description, } ), ...( target === '_blank' && { opensInNewTab: true, } ), ...( blockType === 'core/navigation-submenu' && { isTopLevelItem: level === 0, } ), ...( blockType === 'core/navigation-link' && { isTopLevelLink: level === 0, } ), }; } /** * Creates a nested, hierarchical tree representation from unstructured data that * has an inherent relationship defined between individual items. * * For example, by default, each element in the dataset should have an `id` and * `parent` property where the `parent` property indicates a relationship between * the current item and another item with a matching `id` properties. * * This is useful for building linked lists of data from flat data structures. * * @param {Array} dataset linked data to be rearranged into a hierarchical tree based on relational fields. * @param {string} id the property which uniquely identifies each entry within the array. * @param {*} relation the property which identifies how the current item is related to other items in the data (if at all). * @return {Array} a nested array of parent/child relationships */ function createDataTree( dataset, id = 'id', relation = 'parent' ) { const hashTable = Object.create( null ); const dataTree = []; for ( const data of dataset ) { hashTable[ data[ id ] ] = { ...data, children: [], }; if ( data[ relation ] ) { hashTable[ data[ relation ] ] = hashTable[ data[ relation ] ] || {}; hashTable[ data[ relation ] ].children = hashTable[ data[ relation ] ].children || []; hashTable[ data[ relation ] ].children.push( hashTable[ data[ id ] ] ); } else { dataTree.push( hashTable[ data[ id ] ] ); } } return dataTree; }