diff --git a/docs/USAGE.md b/docs/USAGE.md index dce47df..a0327a7 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -23,6 +23,18 @@ The parent block acts as the controller and wrapper. It handles configuration, s | `axis` | string | `'x'` | Carousel axis direction (`'x'` for horizontal, `'y'` for vertical). | | `direction` | string | `'ltr'` | Carousel item direction: `'ltr'` (left-to-right) or `'rtl'` (right-to-left). | | `slidesToScroll` | number | `1` | Number of slides to scroll per navigation action. | +| `lazyLoadImages` | boolean | `true` | Add `loading="lazy"` to images in slides. Slides can opt out via `disableLazyLoadImages`. | + +--- + +### Child Block: `carousel-kit/carousel-slide` +The child block that serves as a container for individual slide content. Each slide can display custom blocks as defined by the parent's `allowedSlideBlocks` configuration. + +#### Attributes + +| Attribute | Type | Default | Description | +| :-------------------------- | :------ | :------------ | :------------------------------------------ | +| `disableLazyLoadImages` | boolean | `false` | Disable lazy loading for images in this slide (when carousel lazy loading is enabled). | --- diff --git a/inc/Plugin.php b/inc/Plugin.php index a296e70..5a3880a 100644 --- a/inc/Plugin.php +++ b/inc/Plugin.php @@ -10,6 +10,8 @@ namespace Rt_Carousel; use Rt_Carousel\Traits\Singleton; +use WP_Block; +use WP_HTML_Tag_Processor; // Exit if accessed directly. if ( ! defined( 'ABSPATH' ) ) { @@ -41,6 +43,7 @@ protected function setup_hooks(): void { add_action( 'init', [ $this, 'register_block_patterns' ] ); add_action( 'admin_notices', [ $this, 'legacy_plugin_notice' ] ); add_action( 'network_admin_notices', [ $this, 'legacy_plugin_notice' ] ); + add_filter( 'render_block_rt-carousel/carousel', [ $this, 'handle_lazy_load_images' ], 16, 3 ); } /** @@ -260,4 +263,57 @@ private function load_patterns_from_disk(): array { return $data; } + + /** + * Add loading="lazy" to images in carousel slides. + * + * @param string $block_content The block content. + * @param array $parsed_block The parsed block. + * @param \WP_Block $instance The block instance. + * + * @return string Modified block content. + */ + public function handle_lazy_load_images( string $block_content, array $parsed_block, WP_Block $instance ): string { + // $instance was added in WP 5.9.0, if it's not available, return the block content unmodified. + if ( ! $instance ) { + return $block_content; + } + + // Bail early if the lazyLoadImages setting is not set. + if ( ! isset( $instance->attributes['lazyLoadImages'] ) ) { + return $block_content; + } + + $lazy_load = (bool) $instance->attributes['lazyLoadImages']; + + // If lazy loading is disabled, return as-is. + if ( ! $lazy_load ) { + return $block_content; + } + + // Use WP_HTML_Tag_Processor to add loading="lazy" to tags. + $processor = new WP_HTML_Tag_Processor( $block_content ); + $slide_index = 0; + + while ( $processor->next_tag( ) ) { + $tag = $processor->get_tag(); + + // Keep a track of the slide index to determine if an image is in the first slide or subsequent slides. + if ( 'DIV' === $tag && $processor->has_class( 'embla__slide' ) ) { + $slide_index++; + } + + // If it's the first slide, set loading="lazy". For subsequent slides, set loading="eager" and fetchpriority="high". + if ( 'IMG' === $tag && null === $processor->get_attribute( 'loading' ) ) { + if ( 1 === $slide_index ) { + $processor->set_attribute( 'loading', 'eager' ); + $processor->set_attribute( 'fetchpriority', 'high' ); + } else { + $processor->set_attribute( 'loading', 'lazy' ); + } + } + } + + return $processor->get_updated_html(); + } } diff --git a/src/blocks/carousel/__tests__/types.test.ts b/src/blocks/carousel/__tests__/types.test.ts index aed1c05..6a063d7 100644 --- a/src/blocks/carousel/__tests__/types.test.ts +++ b/src/blocks/carousel/__tests__/types.test.ts @@ -33,6 +33,7 @@ describe( 'CarouselAttributes Type', () => { ariaLabel: 'Image carousel', slideGap: 16, slidesToScroll: '1', + lazyLoadImages: true, }; expect( attributes ).toBeDefined(); @@ -58,6 +59,7 @@ describe( 'CarouselAttributes Type', () => { ariaLabel: '', slideGap: 0, slidesToScroll: 'auto', + lazyLoadImages: false, }; // Verify all keys exist diff --git a/src/blocks/carousel/block.json b/src/blocks/carousel/block.json index e0df7ca..3fd53c1 100644 --- a/src/blocks/carousel/block.json +++ b/src/blocks/carousel/block.json @@ -85,10 +85,14 @@ "slidesToScroll": { "type": "string", "default": "1" + }, + "lazyLoadImages": { + "type": "boolean", + "default": true } }, "editorScript": "file:./index.js", "editorStyle": "file:./index.css", "style": "file:./style-index.css", "viewScriptModule": "file:./view.js" -} \ No newline at end of file +} diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx index 9edf641..a85a010 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -50,6 +50,7 @@ export default function Edit( { autoplayStopOnMouseEnter, ariaLabel, slidesToScroll = '1', + lazyLoadImages, } = attributes; const [ emblaApi, setEmblaApi ] = useState(); @@ -241,6 +242,15 @@ export default function Edit( { onChange={ ( value ) => setAttributes( { dragFree: value } ) } help={ __( 'Enables momentum scrolling.', 'rt-carousel' ) } /> + setAttributes( { lazyLoadImages: value } ) } + help={ __( + 'Load images only when they enter the viewport.', + 'carousel-kit', + ) } + /> ; export type CarouselSlideAttributes = { verticalAlignment?: BlockVerticalAlignmentToolbar.Value; + disableLazyLoadImages?: boolean; }; export type CarouselControlsAttributes = Record; export type CarouselDotsAttributes = Record;