Featured List

block.json

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wlc/featured-list",
  "title": "Featured List",
  "category": "widgets",
  "icon": "editor-ul",
  "description": "A block to display featured list",
  "keywords": [
    "featured", "list"
  ],
  "allowedBlocks": [
    "wlc/featured-list-item"
  ],
  "attributes": {
    "layout": {
      "type": "string",
      "default": "columns"
    },
    "itemsPerRow": {
      "type": "string",
      "default": 2
    },
    "connectorsWidth": {
      "type": "string",
      "default": 2
    },
    "gap": {
      "type": "string",
      "default": "20px"
    },
    "iconWrapperWidth": {
      "type": "string",
      "default": 100
    },
    "iconWrapperShape": {
      "type": "string",
      "default": 10
    },
    "iconWrapperPadding": {
      "type": "string",
      "default": 20
    }
  },
  "version": "1.0.0",
  "textdomain": "WLC",
  "editorStyle": "file:./index.css",
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css"
}

edit.js

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';
import {
  useBlockProps,
  useInnerBlocksProps,
  InspectorControls,
  BlockControls,
  Inserter,
} from '@wordpress/block-editor';
import {
  PanelBody,
  Toolbar,
  Button,
  SelectControl,
  __experimentalUnitControl as UnitControl,
  RangeControl,
} from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import { dispatch, select } from '@wordpress/data';
import { createBlock } from '@wordpress/blocks';

const edit = ( { attributes, setAttributes, clientId } ) => {
  const { layout, itemsPerRow, connectorsWidth, gap, iconWrapperWidth, iconWrapperShape, iconWrapperPadding } = attributes;

  useEffect( () => {
    const { replaceInnerBlocks } = dispatch( 'core/block-editor' );
    const { getBlocks } = select( 'core/block-editor' );

    if ( ! getBlocks( clientId ).length) {
      const innerBlocks = [
        createBlock( 'wlc/featured-list-item', { iconWrapperWidth, iconWrapperShape, iconWrapperPadding } ),
        createBlock( 'wlc/featured-list-item', { iconWrapperWidth, iconWrapperShape, iconWrapperPadding } ),
        createBlock( 'wlc/featured-list-item', { iconWrapperWidth, iconWrapperShape, iconWrapperPadding } ),
      ];
      replaceInnerBlocks( clientId, innerBlocks, false );
    }
  }, [ clientId ] );

  const blockProps = useBlockProps( {
    style: {
      '--gap': gap,
      '--icon-width': `${iconWrapperWidth}px`,
      '--icon-shape': `${iconWrapperShape}px`,
      '--icon-padding': `${iconWrapperPadding}px`,
      ...(layout === 'connectors' && { '--connectors-width': `${connectorsWidth}px` }),
    },
  } );

  const innerBlocksProps = useInnerBlocksProps(
    blockProps,
    {
      allowedBlocks: [ 'wlc/featured-list-item' ]
    }
  );

  function Appender( { rootClientId } ) {
    return (
      <Inserter
        rootClientId={ rootClientId}
        renderToggle={ ( { onToggle, disabled } ) => (
          <Button
            className="featured-list-appender"
            onClick={ onToggle }
            disabled={ disabled }
            label={ __( 'Add item', 'WLC' ) }
            icon="plus"
          />
        ) }
        isAppender
      />
    );
  }

  const wrapperClassName = `wlc-featured-list__wrapper layout-${layout}` + (layout === 'row' ? ` items-per-row-${itemsPerRow}` : '');

  return (
    <div { ...blockProps }>
      <InspectorControls>
        <PanelBody title={ __( 'Featured List Settings', 'WLC' ) }>
          <SelectControl
            label={ __( 'Section Layout', 'WLC' ) }
            value={ layout }
            options={ [
              { label: __( 'Columns', 'WLC' ), value: 'columns' },
              { label: __( 'Row', 'WLC' ), value: 'row' },
              { label: __( 'Connectors', 'WLC' ), value: 'connectors' },
            ] }
            onChange={ ( value ) => setAttributes( { layout: value } ) }
          />
          { layout == 'row' && (
            <RangeControl
              label={ __( 'Items Per Row', 'WLC' ) }
              value={ itemsPerRow }
              onChange={ ( value ) => setAttributes( { itemsPerRow: value } ) }
              min={ 2 }
              max={ 6 }
            />
          ) }
          { layout == 'connectors' && (
            <RangeControl
              label={ __( 'Connectors Width (px)', 'WLC' ) }
              value={ connectorsWidth }
              onChange={ ( value ) => setAttributes( { connectorsWidth: value } ) }
              min={ 1 }
              max={ 15 }
            />
          ) }
          <UnitControl
            label={ __( 'Gap Between Items (px)', 'WLC' ) }
            value={ gap }
            onChange={ ( value ) => setAttributes( { gap: value } ) }
            min={ 0 }
            max={ 50 }
          />
          <RangeControl
            label={ __( 'Icons Wrapper Width (px)', 'WLC' ) }
            value={ iconWrapperWidth }
            onChange={ ( value ) => {
              setAttributes( { iconWrapperWidth: value } );

              // Update all child blocks with the new iconWrapperWidth
              const blocks = select( 'core/block-editor' ).getBlocks( clientId );
              blocks.forEach( block => {
                dispatch( 'core/block-editor' ).updateBlockAttributes( block.clientId, { iconWrapperWidth: value } );
              } );
            } }
            min={ 0 }
            max={ 200 }
          />
          <RangeControl
            label={ __( 'Icons Wrapper Radius (px)', 'WLC' ) }
            value={ iconWrapperShape }
            onChange={ ( value ) => {
              setAttributes( { iconWrapperShape: value } );

              // Update all child blocks with the new iconWrapperShape
              const blocks = select( 'core/block-editor' ).getBlocks( clientId );
              blocks.forEach( block => {
                dispatch( 'core/block-editor' ).updateBlockAttributes( block.clientId, { iconWrapperShape: value } );
              } );
            } }
            min={ 0 }
            max={ 50 }
          />
          <RangeControl
            label={ __( 'Icons Wrapper Padding (px)', 'WLC' ) }
            value={ iconWrapperPadding }
            onChange={ ( value ) => {
              setAttributes( { iconWrapperPadding: value } );

              // Update all child blocks with the new iconWrapperPadding
              const blocks = select( 'core/block-editor' ).getBlocks( clientId );
              blocks.forEach( block => {
                dispatch( 'core/block-editor' ).updateBlockAttributes( block.clientId, { iconWrapperPadding: value } );
              } );
            } }
            min={ 0 }
            max={ 60 }
          />
        </PanelBody>
      </InspectorControls>
      <BlockControls>
        <Toolbar label={ __( 'Options', 'WLC' ) }>
          <Appender rootClientId={ clientId } />
        </Toolbar>
      </BlockControls>
      <div { ...innerBlocksProps } className={ wrapperClassName }>
        { innerBlocksProps.children }
        <Appender rootClientId={ clientId } />
      </div>
    </div>
  );
}

export default edit;

editor.scss

.wlc-featured-list__wrapper .featured-list-appender {
  width: 100%;
  border: 1px solid var(--wp-components-color-foreground, #1e1e1e);
  grid-column: 1 / -1;
}

index.js

/**
 * WordPress dependencies
 */
import { registerBlockType } from '@wordpress/blocks';

/**
 * Internal dependencies
 */
import './editor.scss';
import './style.scss';
import edit from './edit';
import save from './save';
import metadata from './block.json';

registerBlockType( metadata.name, {
  edit,
  save,
} );

save.js

/**
 * WordPress dependencies
 */
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';

export default function save( { attributes } ) {
  const { layout, itemsPerRow, connectorsWidth, gap, iconWrapperWidth, iconWrapperShape, iconWrapperPadding } = attributes;

  const blockProps = useBlockProps.save( {
    style: {
      '--gap': gap,
      '--icon-width': `${iconWrapperWidth}px`,
      '--icon-shape': `${iconWrapperShape}px`,
      '--icon-padding': `${iconWrapperPadding}px`,
      ...(layout === 'connectors' && { '--connectors-width': `${connectorsWidth}px` }),
    },
  } );

  const wrapperClassName = `wlc-featured-list__wrapper layout-${layout}` + (layout === 'row' ? ` items-per-row-${itemsPerRow}` : '');

  return (
    <div { ...blockProps }>
      <div className={ wrapperClassName }>
        <InnerBlocks.Content />
      </div>
    </div>
  );
}

style.scss

.wlc-featured-list__wrapper {
  gap: var(--gap);

  &.layout-columns {
    display: flex;
    flex-direction: column;
  }

  &.layout-row {
    display: grid;

    @for $i from 2 through 6 {
      &.items-per-row-#{$i} {
        grid-template-columns: repeat(#{$i}, 1fr);
      }
    }
  }

  &.layout-connectors {
    display: flex;
    flex-direction: column;

    .wlc-featured-list-item {
      &:not(:last-of-type) {
        &::after {
          content: "";
          bottom: 0;
          left: calc(calc(var(--icon-width) / 2) - calc(var(--connectors-width) / 2));
          width: var(--connectors-width);
          height: var(--gap);
          background: var(--connectors-color);
          transform: translateY(100%);
          position: absolute;
        }
      }
    }
  }
}