/** * External dependencies */ import { useCallback, useEffect, useMemo, useRef, useState, } from '@wordpress/element'; import { useAsyncList } from '@wordpress/compose'; import { useSelect, useDispatch, select } from '@wordpress/data'; import { BlockInstance, cloneBlock } from '@wordpress/blocks'; import { close } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; import { getNewPath, navigateTo } from '@woocommerce/navigation'; import { capitalize } from 'lodash'; import { Button, Spinner } from '@wordpress/components'; import { unlock, // @ts-expect-error No types for this exist yet. } from '@wordpress/edit-site/build-module/lock-unlock'; // @ts-expect-error No types for this exist yet. // eslint-disable-next-line @woocommerce/dependency-group import { useIsSiteEditorLoading } from '@wordpress/edit-site/build-module/components/layout/hooks'; // eslint-disable-next-line @woocommerce/dependency-group import { store as coreStore, // @ts-expect-error No types for this exist yet. } from '@wordpress/core-data'; // eslint-disable-next-line @woocommerce/dependency-group import { __experimentalBlockPatternsList as BlockPatternList, store as blockEditorStore, // @ts-expect-error No types for this exist yet. } from '@wordpress/block-editor'; /** * Internal dependencies */ import { usePatternsByCategory } from '../../hooks/use-patterns'; import './style.scss'; import { useEditorBlocks } from '../../hooks/use-editor-blocks'; import { PATTERN_CATEGORIES } from './categories'; import { THEME_SLUG } from '~/customize-store/data/constants'; import { Pattern } from '~/customize-store/types/pattern'; import { findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate, PRODUCT_HERO_PATTERN_BUTTON_STYLE, } from '../../utils/hero-pattern'; import { useIsActiveNewNeutralVariation } from '../../hooks/use-is-active-new-neutral-variation'; /** * Adds a 'is-added' CSS class to each pattern preview element in the pattern list that matches a block's pattern name. * This function iterates through an array of blocks added in the page, extracts the pattern name from each block's metadata, * and finds the corresponding pattern preview element in the pattern list by its ID. If found, the 'is-added' class is added to the element. */ const addIsAddedClassToPatternPreview = ( patternListEl: HTMLElement, blocks: BlockInstance[] ) => { patternListEl.querySelectorAll( '.is-added' ).forEach( ( element ) => { element.classList.remove( 'is-added' ); } ); blocks.forEach( ( block ) => { const patterName = block.attributes.metadata?.patternName; if ( ! patterName ) { return; } const element = patternListEl.querySelector( `[id="${ patterName }"]` ); if ( element ) { element.classList.add( 'is-added' ); } } ); }; /** * Sorts patterns by category. For 'intro' and 'about' categories * prioritized DotCom Patterns. For intro category, it also prioritizes the "centered-content-with-image-below" pattern. * For other categories, it simply sorts patterns to prioritize Woo Patterns. */ const sortPatternsByCategory = ( patterns: Pattern[], category: keyof typeof PATTERN_CATEGORIES ) => { const prefix = 'woocommerce-blocks'; if ( category === 'intro' || category === 'about' ) { return patterns.sort( ( a, b ) => { if ( a.name === 'woocommerce-blocks/centered-content-with-image-below' ) { return -1; } if ( b.name === 'woocommerce-blocks/centered-content-with-image-below' ) { return 1; } if ( a.name.includes( prefix ) && ! b.name.includes( prefix ) ) { return 1; } if ( ! a.name.includes( prefix ) && b.name.includes( prefix ) ) { return -1; } return 0; } ); } return patterns.sort( ( a, b ) => { if ( a.name.includes( prefix ) && ! b.name.includes( prefix ) ) { return -1; } if ( ! a.name.includes( prefix ) && b.name.includes( prefix ) ) { return 1; } return 0; } ); }; export const SidebarPatternScreen = ( { category }: { category: string } ) => { const { patterns, isLoading } = usePatternsByCategory( category ); const isActiveNewNeutralVariation = useIsActiveNewNeutralVariation(); const sortedPatterns = useMemo( () => { const patternsWithoutThemePatterns = patterns.filter( ( pattern ) => ! pattern.name.includes( THEME_SLUG ) && pattern.source !== 'pattern-directory/theme' && pattern.source !== 'pattern-directory/core' ); const patternWithPatchedProductHeroPattern = patternsWithoutThemePatterns.map( ( pattern ) => { if ( pattern.name !== 'woocommerce-blocks/just-arrived-full-hero' ) { return pattern; } if ( ! isActiveNewNeutralVariation ) { const blocks = findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate( pattern.blocks, ( block: BlockInstance ) => { block.attributes.style = {}; } ); return { ...pattern, blocks }; } const blocks = findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate( pattern.blocks, ( block: BlockInstance ) => { block.attributes.style = PRODUCT_HERO_PATTERN_BUTTON_STYLE; } ); return { ...pattern, blocks }; } ); return sortPatternsByCategory( patternWithPatchedProductHeroPattern, category as keyof typeof PATTERN_CATEGORIES ); }, [ category, isActiveNewNeutralVariation, patterns ] ); const asyncSortedPatterns = useAsyncList( sortedPatterns ); const [ patternPagination, setPatternPagination ] = useState( 10 ); const refElement = useRef< HTMLDivElement >( null ); const currentTemplate = useSelect( ( sel ) => // @ts-expect-error No types for this exist yet. sel( coreStore ).__experimentalGetTemplateForLink( '/' ), [] ); const [ blocks ] = useEditorBlocks( 'wp_template', currentTemplate?.id ?? '' ); const blockToScroll = useRef< string | null >( null ); const isEditorLoading = useIsSiteEditorLoading(); const isSpinnerVisible = isLoading || isEditorLoading; useEffect( () => { if ( isSpinnerVisible || refElement.current === null ) { return; } // We want to add the is-added class to the pattern preview when the pattern is loaded in the editor and for each mutation. addIsAddedClassToPatternPreview( refElement.current, blocks ); const observer = new MutationObserver( () => { addIsAddedClassToPatternPreview( refElement.current as HTMLElement, blocks ); } ); const previewPatternList = document.querySelector( '.woocommerce-customize-store-edit-site-layout__sidebar-extra__pattern .block-editor-block-patterns-list' ); if ( previewPatternList ) { observer.observe( previewPatternList, { childList: true, } ); } return () => { observer.disconnect(); }; }, [ isLoading, blocks, isSpinnerVisible ] ); useEffect( () => { if ( isEditorLoading ) { return; } const iframe = window.document.querySelector( '.woocommerce-customize-store-assembler > iframe[name="editor-canvas"]' ) as HTMLIFrameElement; const blockList = iframe?.contentWindow?.document.body.querySelector( '.block-editor-block-list__layout' ); const observer = new MutationObserver( () => { if ( blockToScroll.current ) { const block = blockList?.querySelector( `[id="block-${ blockToScroll.current }"]` ); if ( block ) { block.scrollIntoView(); blockToScroll.current = null; } } } ); if ( blockList ) { observer.observe( blockList, { childList: true } ); } return () => { observer.disconnect(); }; }, [ isEditorLoading ] ); // @ts-expect-error No types for this exist yet. const { insertBlocks } = useDispatch( blockEditorStore ); const insertableIndex = useMemo( () => { return blocks.findLastIndex( ( block ) => block.name === 'core/template-part' ); }, [ blocks ] ); const onClickPattern = useCallback( ( pattern ) => { const parsedPattern = unlock( select( blockEditorStore ) ).__experimentalGetParsedPattern( pattern.name ); const cloneBlocks = parsedPattern.blocks.map( ( blockInstance: BlockInstance ) => cloneBlock( blockInstance ) ); insertBlocks( cloneBlocks, insertableIndex, undefined, false ); blockToScroll.current = cloneBlocks[ 0 ].clientId; }, [ insertBlocks, insertableIndex ] ); return (