Carousel

block.json

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wlc/carousel",
  "title": "Carousel",
  "category": "widgets",
  "icon": "images-alt2",
  "description": "A block to display carousel",
  "keywords": [
    "carousel"
  ],
  "allowedBlocks": [
    "wlc/carousel-item"
  ],
  "attributes": {
    "innerBlocks": {
      "type": "array",
      "default": []
    },
    "type": {
      "type": "string",
      "default": "slide"
    },
    "perPage": {
      "type": "integer",
      "default": 3
    },
    "perMove": {
      "type": "integer",
      "default": 1
    },
    "speed": {
      "type": "integer",
      "default": "400"
    },
    "gap": {
      "type": "string",
      "default": "20px"
    },
    "autoplay": {
      "type": "boolean",
      "default": false
    },
    "interval": {
      "type": "string",
      "default": "5000"
    },
    "arrows": {
      "type": "boolean",
      "default": true
    },
    "pagination": {
      "type": "boolean",
      "default": false
    },
    "pauseOnHover": {
      "type": "boolean",
      "default": true
    },
    "tabletBreakpoint": {
      "type": "integer",
      "default": "1025"
    },
    "mobileBreakpoint": {
      "type": "integer",
      "default": "481"
    },
    "perPageTablet": {
      "type": "integer",
      "default": null
    },
    "perPageMobile": {
      "type": "integer",
      "default": null
    }
  },
  "version": "1.0.0",
  "textdomain": "WLC",
  "editorStyle": "file:./index.css",
  "editorScript": "file:./index.js",
  "script": "file:./view.js",
  "style": "file:./style-index.css"
}

edit.js

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import {
  useBlockProps,
  useInnerBlocksProps,
  InspectorControls,
  BlockControls,
  Inserter,
} from '@wordpress/block-editor';
import {
  PanelBody,
  Toolbar,
  SelectControl,
  ToggleControl,
  RangeControl,
  Button,
  __experimentalNumberControl as NumberControl,
  __experimentalUnitControl as UnitControl,
} from '@wordpress/components';
import { useEffect, useRef, useState } from 'react';

/**
 * External dependencies
 */
import Splide from '@splidejs/splide';
import '@splidejs/splide/dist/css/splide.min.css';

export default function edit( { attributes, setAttributes, clientId } ) {
  const { type, perPage, perMove, speed, gap, arrows, pagination, autoplay, interval, pauseOnHover, tabletBreakpoint, mobileBreakpoint, perPageTablet, perPageMobile } = attributes;
  const blockProps = useBlockProps();

  const splideRef = useRef( null );
  const sliderInstance = useRef( null );

  const { replaceInnerBlocks } = useDispatch( 'core/block-editor' );
  const innerBlocks = useSelect(
    ( select ) => select( 'core/block-editor' ).getBlocks( clientId ),
    [ clientId ]
  );

  const [ initialSlides, setInitialSlides ] = useState( [] ) ;

  // Generate initial slides based on perPage
  useEffect( () => {
    const slides = [];
    for ( let i = 0; i < perPage; i++ ) {
      slides.push( [ 'wlc/carousel-item' ] );
    }
    setInitialSlides( slides );
  }, [ perPage ] );

  useEffect(() => {
    const initializeSplide = () => {
      if ( sliderInstance.current ) {
        sliderInstance.current.destroy();
      }
      sliderInstance.current = new Splide( splideRef.current, {
        type,
        perPage,
        perMove,
        speed,
        gap,
        arrows,
        pagination,
        autoplay,
        interval,
        pauseOnHover,
        breakpoints: {
          [tabletBreakpoint]: {
            perPage: perPageTablet || perPage,
          },
          [mobileBreakpoint]: {
            perPage: perPageMobile || perPage,
          },
        }
      } ).mount();
    };
    
    initializeSplide();
  }, [ type, perPage, perMove, speed, gap, arrows, pagination, autoplay, interval, pauseOnHover, tabletBreakpoint, mobileBreakpoint, perPageTablet, perPageMobile, innerBlocks.length ] );

  const { children, ...innerBlocksProps } = useInnerBlocksProps(
    blockProps,
    {
      allowedBlocks: [ 'wlc/carousel-item' ],
      template: initialSlides,
    }
  );

  const addSlide = () => {
    replaceInnerBlocks(
      clientId,
      [ ...innerBlocks, wp.blocks.createBlock( 'wlc/carousel-item' ) ],
      true
    );
  };

  return (
    <div { ...blockProps }>
      <InspectorControls>
        <PanelBody title={ __( 'Carousel Settings', 'WLC' ) }>
          <SelectControl
            label={ __( 'Type', 'WLC' ) }
            value={ type }
            options={[
              { value: 'slide', label: __( 'Slide', 'WLC' ) },
              { value: 'loop', label: __( 'Loop', 'WLC' ) },
              { value: 'fade', label: __( 'Fade', 'WLC' ) }
            ]}
            onChange={ ( value ) => setAttributes( { type: value } ) }
          />
          <RangeControl
            label={ __( 'Slides Per Page', 'WLC' ) }
            value={ perPage }
            onChange={ ( value ) => setAttributes( { perPage: value } ) }
            min={ 1 }
            max={ 8 }
          />
          <RangeControl
            label={ __( 'Slides Per Move', 'WLC' ) }
            value={ perMove }
            onChange={ ( value) => setAttributes( { perMove: value } ) }
            min={ 1 }
            max={ 8 }
          />
          <NumberControl
            label={ __( 'Speed (ms)', 'WLC' ) }
            value={ speed }
            onChange={ ( value ) => setAttributes( { speed: value } ) }
          />
          <UnitControl
            label={ __( 'Slides Gap', 'WLC' ) }
            value={ gap }
            onChange={ ( value ) => setAttributes( { gap: value } ) }
          />
        </PanelBody>
        <PanelBody title={ __( 'Navigation & Pagination', 'WLC' ) } initialOpen={false}>
          <ToggleControl
            label={ __ ( 'Show Arrows', 'WLC' ) }
            checked={ arrows }
            onChange={ ( value ) => setAttributes( { arrows: value } ) }
          />
          <ToggleControl
            label={ __( 'Show Pagination', 'WLC' ) }
            checked={ pagination }
            onChange={ ( value ) => setAttributes( { pagination: value } ) }
          />
        </PanelBody>
        <PanelBody title={ __( 'Autoplay', 'WLC' ) } initialOpen={false}>
          <ToggleControl
            label={ __( 'Autoplay', 'WLC' ) }
            checked={ autoplay }
            onChange={ ( value ) => setAttributes( { autoplay: value } ) }
          />
          { autoplay && (
            <>
              <NumberControl
                label={ __( 'Interval (ms)', 'WLC' ) }
                value={ interval }
                onChange={ ( value ) => setAttributes( { interval: value } ) }
              />
              <ToggleControl
                label={ __( 'Pause on Hover', 'WLC' ) }
                checked={ pauseOnHover }
                onChange={ ( value ) => setAttributes( { pauseOnHover: value } ) }
              />
            </>
          ) }
        </PanelBody>
        <PanelBody title={ __( 'Breakpoints', 'WLC' ) } initialOpen={false}>
          <NumberControl
            label={ __( 'Tablet Breakpoint (px)', 'WLC' ) }
            value={ tabletBreakpoint }
            onChange={ ( value ) => setAttributes( { tabletBreakpoint: value } ) }
          />
          <RangeControl
            label={ __( 'Slides Per Page (Tablet)', 'WLC') }
            value={ perPageTablet }
            onChange={ ( value ) => setAttributes( { perPageTablet: value } ) }
            min={ 1 }
            max={ 8 }
          />
          <NumberControl
            label={ __( 'Mobile Breakpoint (px)', 'WLC' ) }
            value={ mobileBreakpoint }
            onChange={ ( value ) => setAttributes( { mobileBreakpoint: value } ) }
          />
          <RangeControl
            label={ __( 'Slides Per Page (Mobile)', 'WLC' ) }
            value={ perPageMobile }
            onChange={ ( value) => setAttributes( { perPageMobile: value } ) }
            min={ 1 }
            max={ 8 }
          />
        </PanelBody>
      </InspectorControls>
      <BlockControls>
        <Toolbar label={ __( 'Options', 'WLC' ) }>
          <Button
            onClick={ addSlide }
            label={ __( 'Add slide', 'WLC' ) }
            icon="plus"
          />
        </Toolbar>
      </BlockControls>
      <div
        ref={ splideRef }
        className="splide"
      >
        <div className="splide__track">
          <ul { ...innerBlocksProps } className="splide__list">
            { children }
          </ul>
        </div>
        { arrows && (
          <div className="splide__arrows splide__arrows--ltr"></div>
        ) }
        { pagination && (
          <ul className="splide__pagination splide__pagination--ltr" role="tablist" aria-label={ __( 'Select a slide to show', 'WLC' ) }></ul>
        ) }
      </div>
    </div>
  );
}

editor.scss

@import '@splidejs/splide/dist/css/splide-core.min.css';

.wp-block-wlc-carousel .splide__slide .block-list-appender {
  position: relative;
}

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,
} );

render.php

<?php
/**
 * Renders the html of posts carousel block
 *
 * @package wlc/carousel-block
 */

$posts_type   = $attributes['postType'] ? $attributes['postType'] : 'post';
$number_posts = $attributes['numberPosts'] ? ( (int) $attributes['numberPosts'] ) : 5;
$delay        = $attributes['delay'] ? ( (int) $attributes['delay'] * 1000 ) : 5000;
$arrows       = (bool) $attributes['showArrows'];
$nav          = (bool) $attributes['showNavigation'];

$args = array(
	'post_type'           => $posts_type,
	'post_status'         => 'publish',
	'nopaging'            => true,
	'posts_per_page'      => $number_posts,
	'ignore_sticky_posts' => false,
);

$query = new \WP_Query( $args );
if ( $query->have_posts() ) :
	$settings    = wp_json_encode(
		array(
			'delay'  => $delay,
			'arrows' => $arrows,
			'nav'    => $nav,
		)
	);
	$block_attrs = get_block_wrapper_attributes();
	?>
	<div class="wlc-carousel" data-settings="<?php echo esc_attr( $settings ); ?>" <?php echo wp_kses_data( $block_attrs ); ?>>
		<div class="wlc-carousel__slider splide">
			<div class="splide__track">
				<ul class="splide__list">
					<?php
					while ( $query->have_posts() ) {
						$query->the_post();
						?>
						<li class="splide__slide">
							<div class="wlc-carousel__content">
								<h2 class="wlc-carousel__title"><?php the_title(); ?></h2>
								<span class="wlc-carousel__date"><?php the_date(); ?></span>
								<div class="wlc-carousel__excerpt">
									<?php the_excerpt(); ?>
								</div>
								<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
									<div class="wp-block-button">
										<a href="<?php the_permalink(); ?>" class="wp-block-button__link wp-element-button"><?php echo esc_html__( 'Czytaj więcej', 'WLC' ); ?></a>
									</div>
								</div>
							</div>
							<div class="wlc-carousel__thumbnail">
								<?php the_post_thumbnail(); ?>
							</div>
						</li>
						<?php
					}
					?>
				</ul>
			</div>
			<?php
			if ( $arrows ) :
				?>
				<div class="splide__arrows splide__arrows--ltr"></div>
				<?php
			endif;
			?>
		</div>
		<?php
		if ( $nav ) :
			?>
			<div class="wlc-carousel__slider-nav splide">
				<div class="splide__track">
					<ul class="splide__list">
						<?php
						while ( $query->have_posts() ) {
							$query->the_post();
							?>
							<li class="splide__slide">
								<div class="wlc-carousel-nav-content">
									<p class="wlc-carousel-nav-title"><?php the_title(); ?></p>
								</div>
							</li>
							<?php
						}
						?>
					</ul>
				</div>
			</div>
			<?php
		endif;
		?>
	</div>
	<?php
endif;
wp_reset_postdata();
?>

save.js

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

export default function save( { attributes } ) {
  const { type, perPage, perMove, speed, gap, arrows, pagination, autoplay, interval, pauseOnHover, tabletBreakpoint, mobileBreakpoint, perPageTablet, perPageMobile } = attributes;
  const blockProps = useBlockProps.save( {
    className: 'splide',
    'data-type': type,
    'data-per-page': perPage,
    'data-per-move': perMove,
    'data-speed': speed,
    'data-gap': gap,
    'data-arrows': arrows,
    'data-pagination': pagination,
    'data-autoplay': autoplay,
    'data-interval': interval,
    'data-pause-on-hover': pauseOnHover,
    'data-tablet-breakpoint': tabletBreakpoint,
    'data-mobile-breakpoint': mobileBreakpoint,
    'data-per-page-tablet': perPageTablet,
    'data-per-page-mobile': perPageMobile,
  } );

  return (
    <div className="wlc-carousel">
      <div { ...blockProps }>
        <div className="splide__track">
          <ul className="splide__list">
            <InnerBlocks.Content />
          </ul>
        </div>
      </div>
    </div>
  );
}

style.css

@import '@splidejs/splide/dist/css/splide-core.min.css';

.wlc-carousel {
	@apply px-4;
}

.wlc-carousel__slider {
	@apply relative;

    & .splide__slide {
        @apply grid max-lg:grid-cols-1 lg:grid-cols-[1fr_2fr] max-lg:grid-rows-[auto_auto] lg:grid-rows-1 gap-4;
    }

    & .wlc-carousel__content {
        @apply grid grid-rows-[auto_auto_1fr_auto] col-start-1 max-lg:row-start-2 lg:row-start-1;

		.wp-element-button {
			@apply block;
		}

    }

    & .wlc-carousel__thumbnail {
        @apply max-lg:col-start-1 lg:col-start-2 row-start-1;

		.wp-post-image {
			@apply w-full h-full object-cover rounded-2xl overflow-hidden;
		}

    }

	& .splide__arrows {
		@apply w-full absolute top-[50%] translate-y-[-50%];

		.splide__arrow--prev {
			@apply absolute right-full scale-x-[-1];
		}
		.splide__arrow--next {
			@apply absolute left-full;
		}
	}
}

.wlc-carousel__slider-nav {
	@apply mt-4;

	.wlc-carousel-nav-content {
		@apply p-4 border border-[#ccc] rounded-2xl;

		p {
			@apply p-0 m-0;
		}
	}
}

style.scss

@import '@splidejs/splide/dist/css/splide-core.min.css';

.wlc-carousel {
  @apply px-4;

  & .splide__arrows {
    @apply w-full absolute top-[50%] translate-y-[-50%];

    .splide__arrow--prev {
      @apply absolute right-full scale-x-[-1];
    }

    .splide__arrow--next {
      @apply absolute left-full;
    }

    .splide__arrow:disabled {
      @apply opacity-25 cursor-not-allowed;
    }
  }
}

view.js

import Splide from '@splidejs/splide';

document.addEventListener('DOMContentLoaded', function () {
  const sliders = document.querySelectorAll('.wlc-carousel');
  sliders.forEach(slider => {
    const splideElement = slider.querySelector('.splide');
    
    const options = {
      type: splideElement.getAttribute('data-type'),
      perPage: parseInt(splideElement.getAttribute('data-per-page'), 10),
      perMove: parseInt(splideElement.getAttribute('data-per-move'), 10),
      speed: parseInt(splideElement.getAttribute('data-speed'), 10),
      gap: splideElement.getAttribute('data-gap'),
      arrows: splideElement.getAttribute('data-arrows') === 'true',
      pagination: splideElement.getAttribute('data-pagination') === 'true',
      autoplay: splideElement.getAttribute('data-autoplay') === 'true',
      interval: parseInt(splideElement.getAttribute('data-interval'), 10),
      pauseOnHover: splideElement.getAttribute('data-pause-on-hover') === 'true',
      breakpoints: {
        [parseInt(splideElement.getAttribute('data-tablet-breakpoint'), 10)]: {
          perPage: parseInt(splideElement.getAttribute('data-per-page-tablet'), 10) || parseInt(splideElement.getAttribute('data-per-page'), 10),
        },
        [parseInt(splideElement.getAttribute('data-mobile-breakpoint'), 10)]: {
          perPage: parseInt(splideElement.getAttribute('data-per-page-mobile'), 10) || parseInt(splideElement.getAttribute('data-per-page'), 10),
        },
      }
    };

    new Splide(splideElement, options).mount();
  });
});