Skip to content
Open
16 changes: 10 additions & 6 deletions src/block/design-library/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const Edit = props => {
const spacingSize = ! presetMarks || ! Array.isArray( presetMarks ) ? 120 : presetMarks[ presetMarks.length - 2 ].value

// Replaces the current block with a block made out of attributes.
const createBlockWithAttributes = async ( category, blockName, attributes, innerBlocks, substituteBlocks, parentClientId ) => {
const createBlockWithAttributes = async ( category, blockName, attributes, innerBlocks, substituteBlocks, parentClientId, type ) => {
const disabledBlocks = settings.stackable_block_states || {} // eslint-disable-line camelcase

// Recursively substitute core blocks to disabled Stackable blocks
Expand Down Expand Up @@ -207,7 +207,7 @@ const Edit = props => {
innerBlocks = block[ 0 ].innerBlocks

const isDesignLibraryDevMode = devMode && localStorage.getItem( 'stk__design_library__dev_mode' ) === '1'
if ( ! isDesignLibraryDevMode ) {
if ( ! isDesignLibraryDevMode && type !== 'saved' ) {
if ( category !== 'Header' ) {
if ( ! parentClientId && attributes.hasBackground ) {
attributes.blockMargin = {
Expand Down Expand Up @@ -255,14 +255,16 @@ const Edit = props => {
const blocks = []

for ( const blockDesign of designs ) {
const { designData, category } = blockDesign
const {
designData, category, type,
} = blockDesign

for ( const patterns of designData ) {
const {
name, attributes, innerBlocks,
} = patterns
if ( name && attributes ) {
const block = await createBlockWithAttributes( category, name, applyFilters( 'stackable.design-library.attributes', attributes ), innerBlocks || [], substituteBlocks, parentClientId )
const block = await createBlockWithAttributes( category, name, applyFilters( 'stackable.design-library.attributes', attributes ), innerBlocks || [], substituteBlocks, parentClientId, type )
blocks.push( block )
} else {
console.error( 'Design library selection failed: No block data found' ) // eslint-disable-line no-console
Expand Down Expand Up @@ -336,14 +338,16 @@ const Edit = props => {

_designs.forEach( design => {
const {
designData, blocksForSubstitution, category,
designData, blocksForSubstitution, category, type: designType,
} = design

if ( blocksForSubstitution.size ) {
disabledBlocks = disabledBlocks.union( blocksForSubstitution )
}

designs.push( { designData, category } )
designs.push( {
designData, category, type: designType,
} )
} )

designsRef.current = designs
Expand Down
8 changes: 8 additions & 0 deletions src/components/pro-control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ const LABELS = {
<li>{ __( 'Override styles while keeping them synced', i18n ) }</li>
</ul>,
},
'design-library-saved-patterns': {
title: __( 'Design Library Saved Patterns', i18n ),
description: <ul>
<li>{ __( 'Save entire block layouts in a click', i18n ) }</li>
<li>{ __( 'Apply styling options to each saved pattern', i18n ) }</li>
<li>{ __( 'Import and export your custom patterns across sites', i18n ) }</li>
</ul>,
},
}

const ProControl = props => {
Expand Down
5 changes: 3 additions & 2 deletions src/design-library/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const fetchDesignLibrary = async ( forceReset = false, version = '', type
}
}

return designLibrary[ type ]?.[ version || LATEST_API_VERSION ] ?? designLibrary[ type ]
return designLibrary[ type ]?.[ version || LATEST_API_VERSION ] ?? designLibrary[ type ] ?? {}
}

export const fetchDesign = async designId => {
Expand Down Expand Up @@ -82,8 +82,9 @@ export const filterDesigns = async ( {
library = [],
plan: isPlan = '',
category: isCategory = '',
type = 'patterns',
} ) => {
if ( isPlan ) {
if ( isPlan && type !== 'saved' ) {
library = library.filter( ( { plan } ) => plan === isPlan )
}

Expand Down
38 changes: 38 additions & 0 deletions src/design-library/init.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Stackable_Design_Library {
public function __construct() {
add_action( 'rest_api_init', array( $this, 'register_route' ) );

add_action( 'register_stackable_global_settings', array( $this, 'register_saved_patterns' ) );

add_action( 'stackable_delete_design_library_cache', array( $this, 'delete_cache_v3' ) );

add_filter( 'stackable_localize_script', array( $this, 'add_wp_theme_global_styles' ) );
Expand Down Expand Up @@ -90,6 +92,37 @@ public function register_route() {
) );
}

public function register_saved_patterns() {
register_setting(
'stackable_design_library',
'stackable_design_library_saved_patterns',
array(
'type' => 'array',
'description' => __( 'Stackable Design Library User-Saved Patterns', STACKABLE_I18N ),
'sanitize_callback' => array( $this, 'sanitize_array_setting' ),
'show_in_rest' => array(
'schema' => array(
'items' => array(
'type'=>'object',
'properties'=> array(
'id' => array( 'type' => 'string' ),
'label' => array( 'type' => 'string' ),
'description' => array( 'type' => 'string' ),
'category' => array( 'type' => 'string' ),
'template' => array( 'type' => 'string' )
)
)
)
),
'default' => array(),
)
);
}

public function sanitize_array_setting( $input ) {
return is_array( $input ) ? $input : array();
}

/**
* Deletes all design library v3 caches.
*/
Expand Down Expand Up @@ -289,6 +322,11 @@ public function get_design_library( $request ) {
$this->delete_cache();
}

if ( $type === 'saved' ) {
$saved_patterns = get_option( 'stackable_design_library_saved_patterns', [] );
return rest_ensure_response( $saved_patterns );
}

return rest_ensure_response( $this->get_design_library_from_cloud( $type ) );
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '@wordpress/element'
import { Dashicon, Spinner } from '@wordpress/components'
import { __ } from '@wordpress/i18n'
import { applyFilters } from '@wordpress/hooks'

const DesignLibraryListItem = memo( props => {
const {
Expand All @@ -41,7 +42,7 @@ const DesignLibraryListItem = memo( props => {
? presetMarks[ presetMarks.length - 2 ].value
: 120

const [ isLoading, setIsLoading ] = useState( true )
const [ isLoading, setIsLoading ] = useState( false )
const [ selected, setSelected ] = useState( false )

const {
Expand Down Expand Up @@ -82,27 +83,54 @@ const DesignLibraryListItem = memo( props => {
[ `ugb--is-${ plan }` ]: ! isPro && plan !== 'free',
'ugb--is-toggled': selectedNum,
'ugb--is-hidden': ! shouldRender,
'ugb-design-library-item--pages': selectedTab === 'pages',
} )

const onClickHost = e => {
e.stopPropagation()
if ( selectedTab === 'pages' ) {
return
}
onClickDesign()
}

const isInteractiveTarget = target => {
if ( ! target || ! target.tagName ) {
return false
}

const tag = target.tagName
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable
}

const buttonAttributes = {
tabIndex: 0,
role: 'button',
onClick: onClickHost,
onKeyDown: e => {
if ( isInteractiveTarget( e.target ) ) {
return
}

if ( e.key === 'Enter' || e.key === ' ' ) {
e.preventDefault()
onClickHost( e )
}
},
}

return (
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
<button
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
className={ mainClasses }
ref={ ref }
onClick={ onClickHost }
onMouseOut={ onMouseOut }
onMouseOver={ onMouseOver }
{ ...( selectedTab === 'patterns' ? buttonAttributes : {} ) }
>
{ ! isPro && plan !== 'free' && <span className="stk-pulsating-circle" role="presentation" /> }
<div style={ { position: 'relative' } } className={ `stk-block-design__design-container ${ designPreviewSize > 100 ? 'stk--design-preview-large' : 'stk--design-preview-small' }` }>
<div
style={ { position: 'relative' } }
className={ `stk-block-design__design-container ${ designPreviewSize > 100 ? 'stk--design-preview-large' : 'stk--design-preview-small' }` }
{ ...( selectedTab === 'saved' ? buttonAttributes : {} ) }
>
{ ! isPro && plan !== 'free' && (
<ProControl
type="design-library"
Expand Down Expand Up @@ -133,12 +161,15 @@ const DesignLibraryListItem = memo( props => {
</div>
</div>

{ /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */ }
<footer
// Add the number if isToggle is a number, signifying an order instead of just an on/off.
data-selected-num={ selectedNum }
{ ...( selectedTab === 'saved' ? buttonAttributes : {} ) }
>
<div>
<div className="stk-design-library-item__label-row">
<h4> { label } </h4>
{ selectedTab === 'saved' && isPro && applyFilters( 'stackable.design-library.pattern-label-actions', null, previewProps ) }
{ blocksForSubstitutionRef.current !== false && blocksForSubstitutionRef.current.size !== 0 &&
<Tooltip text={ __( 'This design contains disabled blocks. You can still insert this design with blocks substituted with other enabled blocks.', i18n ) }>
<Dashicon icon="warning" size={ 16 } />
Expand All @@ -151,7 +182,7 @@ const DesignLibraryListItem = memo( props => {
<Dashicon icon="editor-help" size={ 16 } />
</Tooltip>
}
{ selectedTab === 'patterns' ? <span className="stk-block-design__selected-num">{ selectedNum === 0 ? '' : selectedNum }</span>
{ selectedTab !== 'pages' ? <span className="stk-block-design__selected-num">{ selectedNum === 0 ? '' : selectedNum }</span>
: <div>
<Button
label={ __( 'Insert', i18n ) }
Expand All @@ -169,7 +200,7 @@ const DesignLibraryListItem = memo( props => {
}
</div>
</footer>
</button>
</div>
)
} )

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export const DesignPreview = ( {
blocks = '',
shadowRoot,
selectedTab,
designIndex,
onMouseDown = NOOP,
updateShadowBodySize = NOOP,
setIsLoading,
Expand Down Expand Up @@ -55,7 +54,7 @@ export const DesignPreview = ( {

useEffect( () => {
const container = ref.current
if ( ! container || selectedTab === 'patterns' ) {
if ( ! container || selectedTab !== 'pages' ) {
return
}

Expand Down Expand Up @@ -88,30 +87,18 @@ export const DesignPreview = ( {
return
}

setIsLoading( true )
// Prevent interaction and focus within the preview content
wrapper.setAttribute( 'inert', '' )

const ric = window.requestIdleCallback ? ( cb => window.requestIdleCallback( cb, { timeout: 5000 } ) )
: ( cb => setTimeout( cb, designIndex * 20 ) )
const sanitizedHTML = safeHTML( blocks )

if ( selectedTab !== 'pages' || designIndex < 9 ) {
// insert HTML for patterns and for the first 9 pages
wrapper.innerHTML = sanitizedHTML
requestAnimationFrame( () => {
ric( () => setIsLoading( false ) )
} )
return
wrapper.innerHTML = sanitizedHTML

if ( selectedTab === 'pages' ) {
updateShadowBodySize()
}

requestAnimationFrame( () => {
ric( () => {
wrapper.innerHTML = sanitizedHTML
updateShadowBodySize()
requestAnimationFrame( () => {
ric( () => setIsLoading( false ) )
} )
} )
} )
setIsLoading( false )
}, [ blocks, shadowRoot ] ) // Only depend on blocks and shadowRoot; selectedTab and designIndex changes will cause blocks to update

return createPortal( <>
Expand All @@ -121,6 +108,7 @@ export const DesignPreview = ( {
>
<div
ref={ wrapperRef }
className="is-layout-constrained"
style={ { pointerEvents: 'none' } } // prevent blocks from being clicked
/>
</body>
Expand Down
Loading
Loading