woocommerce/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/sidebar-pattern-screen.tsx

363 lines
9.9 KiB
TypeScript
Raw Normal View History

/**
* 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 (
<div
className="woocommerce-customize-store-edit-site-layout__sidebar-extra__pattern"
onScroll={ ( event ) => {
const element = event.target as HTMLElement;
const scrollTop = element.scrollTop;
const percentage =
scrollTop / ( element.scrollHeight - element.clientHeight );
if ( percentage > 0.5 ) {
setPatternPagination( ( prev ) => prev + 10 );
}
} }
>
<div className="woocommerce-customize-store-edit-site-layout__sidebar-extra__pattern__header">
<h1>
{ capitalize(
PATTERN_CATEGORIES[
category as keyof typeof PATTERN_CATEGORIES
].label
) }
</h1>
<Button
onClick={ () => {
const homepageUrl = getNewPath(
{ customizing: true },
`/customize-store/assembler-hub/homepage`,
{}
);
navigateTo( { url: homepageUrl } );
} }
iconSize={ 18 }
icon={ close }
label={ __( 'Close', 'woocommerce' ) }
/>
</div>
<div className="woocommerce-customize-store-edit-site-layout__sidebar-extra__pattern__description">
<span>
{
PATTERN_CATEGORIES[
category as keyof typeof PATTERN_CATEGORIES
].description
}
</span>
</div>
{ isSpinnerVisible && (
<span className="components-placeholder__preview">
<Spinner />
</span>
) }
{ ! isSpinnerVisible && (
<BlockPatternList
shownPatterns={ asyncSortedPatterns.slice(
0,
patternPagination
) }
blockPatterns={ asyncSortedPatterns.slice(
0,
patternPagination
) }
onClickPattern={ onClickPattern }
label={ 'Homepage' }
orientation="vertical"
category={ category }
isDraggable={ false }
showTitlesAsTooltip={ true }
ref={ refElement }
/>
) }
</div>
);
};