diff --git a/package.json b/package.json
index ba6a2d11..0c5f1599 100644
--- a/package.json
+++ b/package.json
@@ -107,6 +107,7 @@
"volto-gdpr-privacy": "2.2.14",
"volto-querywidget-with-browser": "0.4.3",
"volto-rss-block": "3.0.1",
+ "volto-rt-carousel": "1.0.0",
"volto-secondarymenu": "4.1.2",
"volto-site-settings": "0.4.7",
"volto-slate-italia": "1.0.6",
diff --git a/src/components/Blocks/Listing/Carousel/CarouselTemplate.jsx b/src/components/Blocks/Listing/Carousel/CarouselTemplate.jsx
new file mode 100644
index 00000000..ccf33987
--- /dev/null
+++ b/src/components/Blocks/Listing/Carousel/CarouselTemplate.jsx
@@ -0,0 +1,294 @@
+/*
+ * Carousel
+ */
+import 'slick-carousel/slick/slick.css';
+import 'io-sanita-theme/components/slick-carousel/slick/slick-theme.css';
+import { Col, Container, Row } from 'design-react-kit';
+import {
+ ListingImage,
+ ListingContainer,
+} from 'io-sanita-theme/components/Blocks';
+import {
+ LinkMore,
+ SingleSlideWrapper,
+ CarouselWrapper,
+ SliderContainer,
+ ButtonPlayPause,
+ useSlider,
+ GalleryPreview,
+} from 'io-sanita-theme/components';
+
+import React, { useState } from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
+import config from '@plone/volto/registry';
+
+import './carouselTemplate.scss';
+const messages = defineMessages({
+ carouselItemAriaLabel: {
+ id: 'carousel-item-aria-label',
+ defaultMessage:
+ 'Sei attualmente in un carosello, per navigare usa le frecce sinistra e destra',
+ },
+ dots: {
+ id: 'dots',
+ defaultMessage: 'Navigazione elementi slider',
+ },
+ slideDot: {
+ id: 'slideDot',
+ defaultMessage: 'Vai alla slide {index}',
+ },
+ openLink: {
+ id: 'openLink',
+ defaultMessage: 'Apri il link',
+ },
+});
+
+const Slide = (props) => {
+ const intl = useIntl();
+ const { index, appearance, appearanceProp, onKeyDown } = props;
+
+ const appearances = config.blocks.blocksConfig.listing.variations.filter(
+ (v) => v.id === 'carousel',
+ )[0]?.appearance;
+ const SlideItemAppearance = appearances[appearance] ?? appearances['default'];
+
+ return (
+
+
+
+
+
+ );
+};
+
+const CarouselTemplate = (props) => {
+ const {
+ items,
+ title,
+ isEditMode,
+ show_block_bg,
+ linkTitle,
+ linkHref,
+ slidesToShow = '1',
+ full_width = false,
+ show_image_title = true,
+ show_dots = true,
+ autoplay = false,
+ autoplay_speed = 2, //seconds
+ slide_appearance = 'default',
+ reactSlick,
+ block,
+ ...otherProps
+ } = props;
+ const block_id = block;
+ const intl = useIntl();
+
+ const [userAutoplay, setUserAutoplay] = useState(autoplay);
+ const [viewImageIndex, setViewImageIndex] = useState(null);
+ const nSlidesToShow =
+ items.length < parseInt(slidesToShow)
+ ? items.length
+ : parseInt(slidesToShow);
+ const Slider = reactSlick.default;
+ const {
+ slider,
+ focusSlide,
+ SliderNextArrow,
+ SliderPrevArrow,
+ handleSlideKeydown,
+ } = useSlider(userAutoplay, setUserAutoplay, block_id);
+
+ const toggleAutoplay = () => {
+ if (!slider?.current) return;
+ if (userAutoplay) {
+ setUserAutoplay(false);
+ slider.current.slickPause();
+ } else {
+ setUserAutoplay(true);
+ slider.current.slickPlay();
+ }
+ };
+
+ const renderCustomDots = (props) => {
+ // Custom handling of focus for a11y
+ return (
+
+ {props.map((item, index) => {
+ const El = item.type;
+ const children = item.props.children;
+ // Justified assumption: children is an Object and not an Array here
+ const Child =
+ children.type ||
+ function () {
+ return null;
+ };
+ return (
+
+
+
+ );
+ })}
+
+ );
+ };
+
+ const settings = {
+ dots: show_dots,
+ infinite: true,
+ autoplay: autoplay,
+ speed: 500,
+ slidesToShow: nSlidesToShow,
+ slidesToScroll: nSlidesToShow,
+ autoplaySpeed: autoplay_speed * 1000,
+ pauseOnHover: true,
+ pauseOnFocus: true,
+ pauseOnDotsHover: true,
+ swipe: true,
+ swipeToSlide: true,
+ focusOnSelect: true,
+ draggable: true,
+ accessibility: true,
+ nextArrow: ,
+ prevArrow: ,
+ appendDots: renderCustomDots,
+ // Custom handling of focus for a11y
+ afterChange: focusSlide,
+ responsive: [
+ {
+ breakpoint: 980,
+ settings: {
+ slidesToShow: 1,
+ slidesToScroll: 1,
+ },
+ },
+ ],
+ };
+
+ return (
+
+
+
+
+
+ {items?.length > nSlidesToShow && (
+
+ {userAutoplay ? 'pause' : 'play'}
+
+ )}
+
+
+ {items.map((item, index) => {
+ const image = (
+
+ );
+ const nextIndex = index < items.length - 1 ? index + 1 : null;
+ const prevIndex = index > 0 ? index - 1 : null;
+ return (
+
+ );
+ })}
+
+ {viewImageIndex !== null && (
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+CarouselTemplate.propTypes = {
+ items: PropTypes.arrayOf(PropTypes.any).isRequired,
+ linkTitle: PropTypes.any,
+ linkHref: PropTypes.any,
+ isEditMode: PropTypes.bool,
+ title: PropTypes.string,
+};
+
+export default injectLazyLibs(['reactSlick'])(CarouselTemplate);
diff --git a/src/components/Blocks/Listing/Carousel/SlideAppearance/SlideGalleryItem.jsx b/src/components/Blocks/Listing/Carousel/SlideAppearance/SlideGalleryItem.jsx
new file mode 100644
index 00000000..c7cd8070
--- /dev/null
+++ b/src/components/Blocks/Listing/Carousel/SlideAppearance/SlideGalleryItem.jsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { defineMessages } from 'react-intl';
+import { flattenToAppURL } from '@plone/volto/helpers';
+import { UniversalLink } from '@plone/volto/components';
+import { Container } from 'design-react-kit';
+import { Icon } from 'io-sanita-theme/components';
+
+import { ListingImage } from 'io-sanita-theme/components/Blocks';
+import './slideGalleryItem.scss';
+const messages = defineMessages({
+ viewImage: {
+ id: "Vedi l'immagine",
+ defaultMessage: "Vedi l'immagine",
+ },
+ viewPreview: {
+ id: 'gallery_viewPreview',
+ defaultMessage: "Vedi l'anteprima di",
+ },
+});
+const SlideGalleryItem = ({
+ item,
+ index,
+ image,
+ show_image_title,
+ show_image_description,
+ show_image_popup,
+ full_width,
+ intl,
+ setUserAutoplay,
+ userAutoplay,
+ slider,
+ viewImageIndex,
+ setViewImageIndex,
+ items,
+}) => {
+ const imageProps = {
+ item,
+ sizes: `(max-width:600px) 450px, (max-width:1024px) ${
+ items.length < 2 ? '1000' : '500'
+ }px, ${items.length === 1 ? '1300' : items.length === 2 ? '650' : '450'}px`,
+ noWrapLink: true,
+ showDefault: true,
+ };
+
+ const figure = (imageProps, item) => {
+ return (
+
+
+ {(show_image_title ||
+ (show_image_description && (item.description || item.rights))) && (
+
+ {show_image_title && {item.title}}
+ {show_image_description && (item.description || item.rights) && (
+
+ {item.description ?? item.rights}
+
+ )}
+
+ )}
+
+ );
+ };
+
+ return (
+
+ );
+};
+
+export default SlideGalleryItem;
diff --git a/src/components/Blocks/Listing/Carousel/SlideAppearance/SlideItemDefault.jsx b/src/components/Blocks/Listing/Carousel/SlideAppearance/SlideItemDefault.jsx
new file mode 100644
index 00000000..e4dc0407
--- /dev/null
+++ b/src/components/Blocks/Listing/Carousel/SlideAppearance/SlideItemDefault.jsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { defineMessages } from 'react-intl';
+import { UniversalLink } from '@plone/volto/components';
+import { Container } from 'design-react-kit';
+import { Icon } from 'io-sanita-theme/components';
+
+const messages = defineMessages({
+ openLink: {
+ id: 'openLink',
+ defaultMessage: 'Apri il link',
+ },
+});
+const SlideItemDefault = ({
+ item,
+ index,
+ image,
+ show_image_title,
+ full_width,
+ intl,
+ setUserAutoplay,
+ userAutoplay,
+ slider,
+}) => {
+ return (
+
+ {image ? (
+ {image}
+ ) : (
+
+ )}
+ {show_image_title && (
+
+
+ {full_width ? (
+
+ {item.title}{' '}
+
+
+ ) : (
+ <>
+ {item.title}{' '}
+
+ >
+ )}
+
+
+ )}
+
+ );
+};
+
+export default SlideItemDefault;
diff --git a/src/components/Blocks/Listing/Carousel/SlideAppearance/slideGalleryItem.scss b/src/components/Blocks/Listing/Carousel/SlideAppearance/slideGalleryItem.scss
new file mode 100644
index 00000000..05a49ea8
--- /dev/null
+++ b/src/components/Blocks/Listing/Carousel/SlideAppearance/slideGalleryItem.scss
@@ -0,0 +1,25 @@
+.photogallery-item {
+ .img-wrapper {
+ position: relative;
+
+ &.responsive {
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+
+ figcaption {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ background-color: #ffffffa7;
+
+ padding: 0.5em 1em;
+ color: #666;
+ font-size: 0.8em;
+ text-align: center;
+ }
+ }
+}
diff --git a/src/components/Blocks/Listing/Carousel/carouselTemplate.scss b/src/components/Blocks/Listing/Carousel/carouselTemplate.scss
new file mode 100644
index 00000000..d6be7c2d
--- /dev/null
+++ b/src/components/Blocks/Listing/Carousel/carouselTemplate.scss
@@ -0,0 +1,219 @@
+@import 'io-sanita-theme/theme/bootstrap-italia-base-config';
+$slider-height: 400px;
+$slider-fullwidth-height: 600px;
+$slider-multislide-height: 300px;
+$slider-mobile-height: 300px;
+
+@mixin carousel-height($height) {
+ .slick-track {
+ min-height: $height;
+ }
+
+ .slick-slide {
+ .slide-wrapper {
+ figure.img-wrapper,
+ .img-placeholder {
+ height: $height;
+ img {
+ min-height: $height;
+ }
+ }
+ }
+ }
+}
+
+.carouselTemplate {
+ margin: 40px 0;
+
+ .slider-container {
+ .slick-track {
+ display: flex;
+ align-items: center;
+ }
+
+ .slick-slide {
+ .slide-wrapper {
+ position: relative;
+ margin: 0 auto;
+
+ figure.img-wrapper {
+ position: relative;
+ overflow: hidden;
+ width: 100%;
+ margin: 0;
+
+ img {
+ min-width: 100%;
+ }
+
+ .volto-image.responsive img,
+ img {
+ object-fit: cover;
+ }
+
+ figcaption {
+ padding: 0.5em 1em;
+ color: #666;
+ font-size: 0.8em;
+ text-align: center;
+ }
+ }
+
+ .img-placeholder {
+ height: 400px;
+ background-color: rgba(0, 0, 0, 0.15);
+ }
+
+ .slide-title {
+ position: absolute;
+ right: auto;
+ bottom: 0;
+ left: auto;
+ width: 100%;
+ padding: 0.7rem 1.2rem;
+ margin: 0 auto;
+
+ background-color: #3f4142e0;
+
+ .slide-link {
+ color: $white !important;
+ }
+
+ font-size: 1.8rem;
+ font-weight: bold;
+ text-decoration: none;
+
+ &:hover,
+ &:active {
+ text-decoration-line: underline;
+ }
+
+ .icon {
+ margin-left: 0.5em;
+ }
+ }
+ }
+ }
+
+ @include carousel-height($slider-height);
+
+ &.full-width {
+ @include carousel-height($slider-fullwidth-height);
+ }
+ }
+
+ &.slidesToShow-2,
+ &.slidesToShow-3,
+ &.slidesToShow-4,
+ &.slidesToShow-5,
+ &.slidesToShow-6 {
+ .slider-container {
+ .slick-slide {
+ margin-right: 0.65rem;
+ margin-left: 0.65rem;
+ }
+ @include carousel-height($slider-multislide-height);
+ }
+ }
+
+ &.no-margin {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ &.appearance_simple_card,
+ &.appearance_image_card {
+ .slider-container {
+ .slick-track {
+ align-items: stretch;
+ padding-bottom: 1.5rem;
+
+ .slick-slide {
+ height: auto;
+
+ > div {
+ height: 100%;
+ }
+
+ .it-single-slide-wrapper {
+ height: 100%;
+
+ .slide-wrapper {
+ height: 100%;
+
+ > .card,
+ > .card-wrapper {
+ height: 100%;
+ }
+
+ > .card {
+ margin: 0.5rem 0;
+ }
+
+ .shadow,
+ .card-bg {
+ box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.15) !important;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ &.appearance_image_card {
+ .slider-container {
+ .slick-track {
+ align-items: start;
+ }
+
+ .slick-slide {
+ .slide-wrapper {
+ .listing-item {
+ margin-top: 0.5rem;
+
+ .img-responsive-wrapper {
+ .img-wrapper {
+ position: absolute;
+ height: 100%;
+
+ img {
+ min-width: unset !important;
+ min-height: unset !important;
+ }
+ }
+
+ &.natural-image-size {
+ .img-responsive {
+ position: static;
+ height: auto;
+ padding: 0;
+
+ .img-wrapper {
+ position: static;
+ margin: 0;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @media (max-width: #{map-get($grid-breakpoints, sm)}) {
+ .slider-container,
+ .slider-container.full-width {
+ @include carousel-height($slider-mobile-height);
+
+ .slick-slide {
+ .slide-wrapper {
+ .slide-title {
+ font-size: 1.3rem;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/components/Blocks/Listing/Skeletons/CarouselTemplateSkeleton.jsx b/src/components/Blocks/Listing/Skeletons/CarouselTemplateSkeleton.jsx
new file mode 100644
index 00000000..848e466b
--- /dev/null
+++ b/src/components/Blocks/Listing/Skeletons/CarouselTemplateSkeleton.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import CarouselTemplate from 'io-sanita-theme/components/Blocks/Listing/Carousel/CarouselTemplate';
+
+const CarouselTemplateSkeleton = (data) => {
+ let items = [];
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach((i) => {
+ items.push({ '@id': i + '' });
+ });
+ return (
+
+
+
+ );
+};
+
+CarouselTemplateSkeleton.propTypes = {
+ linkHref: PropTypes.any,
+ isEditMode: PropTypes.bool,
+ title: PropTypes.string,
+};
+
+export default CarouselTemplateSkeleton;
diff --git a/src/components/GalleryPreview/GalleryPreview.jsx b/src/components/GalleryPreview/GalleryPreview.jsx
index 4d7ab6b5..59e783ad 100644
--- a/src/components/GalleryPreview/GalleryPreview.jsx
+++ b/src/components/GalleryPreview/GalleryPreview.jsx
@@ -72,7 +72,7 @@ const GalleryPreview = ({ id, viewIndex, setViewIndex, items }) => {
{image.title}
- {image.description && {image.description}
}
+ {image.description && {image.description}
}
{items.length > 1 && (