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 ( + + ); + }; + + 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 ( +
+ {!show_image_popup ? ( + + {figure(imageProps, item)} + + ) : ( + { + e.preventDefault(); + e.stopPropagation(); + setViewImageIndex(index); + }} + onKeyDown={(e) => { + if (e.keyCode === 13) { + e.preventDefault(); + e.stopPropagation(); + setViewImageIndex(index); + } + }} + aria-label={`${intl.formatMessage(messages.viewPreview)} ${ + item.title + }`} + > + {figure(imageProps, item)} + + )} +
+ ); +}; + +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 && (