diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.Card.fusion b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.Card.fusion new file mode 100644 index 000000000..d69ba20ff --- /dev/null +++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.Card.fusion @@ -0,0 +1,14 @@ +prototype(Neos.Presentation:Molecule.Carousel.Card) < prototype(Neos.Fusion:Component) { + + headline = '' + text = '' + link = '' + + renderer = afx` +
+ + + +
+ ` +} diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.fusion b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.fusion new file mode 100644 index 000000000..4faef6111 --- /dev/null +++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Carousel/Carousel.fusion @@ -0,0 +1,65 @@ +prototype(Neos.Presentation:Carousel) < prototype(Neos.Fusion:Component) { + + @styleguide { + title = 'Carousel' + props { + headline = 'A nice looking carousel!' + cards = Neos.Fusion:DataStructure { + 0 { + headline = "Card One Headline" + text = "Card One Text" + link = "Card One Link" + } + 1 { + headline = "Card Two Headline" + text = "Card Two Text" + link = "Card Two Link" + } + 2 { + headline = "Card Three Headline" + text = "Card Three Text" + link = "Card Three Link" + } + 3 { + headline = "Card Four Headline" + text = "Card Four Text" + link = "Card Four Link" + } + } + } + } + + @propTypes { + headline = PropTypes:String + cards = PropTypes:Array { + type = PropTypes:DataStructure { + headline = PropTypes:String + text = PropTypes:String + link = PropTypes:String + } + } + } + + @private { + cards = Neos.Fusion:Map { + items = ${props.cards} + itemRenderer = afx` + + ` + } + } + + options = Neos.Fusion:DataStructure { + perPage = 3 + perMove = 1 + } + + renderer = afx` +
+
+ +
+ +
+ ` +} diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Fragment/Item.fusion b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Fragment/Item.fusion new file mode 100644 index 000000000..e4cab7aae --- /dev/null +++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Fragment/Item.fusion @@ -0,0 +1,17 @@ +prototype(Neos.Presentation:Slider.Fragment.Item) < prototype(Neos.Fusion:Component) { + videoUri = null + youtubeId = null + vimdeoId = null + content = null + class = 'flex flex-col items-center justify-center' + renderer = afx` +
  • + {props.content} +
  • + ` +} diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.fusion b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.fusion new file mode 100644 index 000000000..914c8ac50 --- /dev/null +++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.fusion @@ -0,0 +1,54 @@ +prototype(Neos.Presentation:Slider) < prototype(Neos.Fusion:Component) { + // This is used for the living styleguide (Monocle) + // Read more about this in the README.md + @styleguide.props.items = Neos.Fusion:Map { + items = ${Array.range(1, 10)} + itemRenderer = afx` + placeholder image + ` + } + + tagName = 'section' + sliderIsDecoration = false + class = null + slideItemClass = 'flex flex-col items-center justify-center' + + options = Neos.Fusion:DataStructure { + # The gap between slides. The CSS format is acceptable, such as 1em. + gap = 12 + } + + attributes = Neos.Fusion:DataStructure + + i18n = Neos.Fusion:Map { + items = ${['prev', 'next', 'first', 'last', 'slideX', 'pageX', 'play', 'pause', 'carousel', 'select', 'slide', 'slideLabel', 'playVideo']} + keyRenderer = ${item} + itemRenderer = ${I18n.translate('Neos.Presentation:Main:splide.' + item)} + } + + _hasItems = ${Type.isArray(this.items) && Array.length(this.items)} + @if.hasItemsOrContent = ${this._hasItems || this.content} + + renderer = Neos.Fusion:Tag { + tagName = ${props.tagName} + attributes { + x-data = 'slider' + data-splide = ${Json.stringify(Array.concat({i18n:props.i18n}, props.options))} + aria-label = ${props.label} + role = ${props.sliderIsDecoration ? 'group' : null} + class = ${Array.push('splide', props.class)} + @apply.attributes = ${props.attributes} + } + content = afx` +
    + + + {props.content} +
    + ` + } +} diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.js b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.js new file mode 100644 index 000000000..b2d0350c7 --- /dev/null +++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.js @@ -0,0 +1,62 @@ +import Alpine from 'alpinejs'; +import Splide from '@splidejs/splide'; +import { Video } from '@splidejs/splide-extension-video'; + +function getFirstNode(nodeList) { + return [...nodeList].filter((node) => node.tagName === 'LI')[0]; +} + +function getIndexOfElement(element) { + return Array.from(element.parentElement.children).indexOf(element); +} + +Alpine.data('slider', () => ({ + init() { + const rootElement = this.$root; + const splide = new Splide(rootElement); + const inNeosBackend = window.name === 'neos-content-main'; + + // We are in the backend, so we need to refresh the instance on change + if (inNeosBackend) { + splide.on('mounted', function () { + // Update if a slide is added or removed + const observeTarget = rootElement.querySelector('.splide__list'); + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + const addedNode = getFirstNode(mutation.addedNodes); + const removedNode = getFirstNode(mutation.removedNodes); + if (addedNode || removedNode) { + console.log('Refreshing instance'); + splide.refresh(); + } + if (addedNode) { + // Scroll to the new slide + splide.go(getIndexOfElement(addedNode)); + } + }); + }); + observer.observe(observeTarget, { childList: true }); + + // Go to the slide if it gets selceted in the node tree + document.addEventListener('Neos.NodeSelected', (event) => { + const element = event.detail.element; + if (!element.classList.contains('splide__slide')) { + return; + } + splide.go(getIndexOfElement(element)); + }); + }); + } + + splide.mount({ Video }); + // Disable the play button in the backend + splide.Components.Video.disable(inNeosBackend); + const maxIndex = splide.length - 1; + splide.on('autoplay:playing', (rate) => { + // Go to the first slide after the last slide + if (rate === 1 && maxIndex === splide.index) { + splide.go(0); + } + }); + }, +})); diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.pcss b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.pcss new file mode 100644 index 000000000..59f4f2971 --- /dev/null +++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Molecule/Slider/Slider.pcss @@ -0,0 +1,19 @@ +@import "@splidejs/splide/dist/css/splide-core.min.css"; +@import "@splidejs/splide/dist/css/themes/splide-default.min.css"; +@import "@splidejs/splide-extension-video/dist/css/splide-extension-video.min.css"; + +.splide__pagination__page.is-active { + background: rgb(50, 50, 50); +} + +.splide__slide { + & > figure { + width: 100%; + margin: 0; + + & > img { + width: 100%; + height: auto; + } + } +} diff --git a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Scripts/index.ts b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Scripts/index.ts index b77cf77ce..d61fc28b8 100644 --- a/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Scripts/index.ts +++ b/DistributionPackages/Neos.Presentation/Resources/Private/Fusion/Scripts/index.ts @@ -8,6 +8,7 @@ import collapse from '@alpinejs/collapse'; import clipboard from '@ryangjchandler/alpine-clipboard'; import typewriter from '@marcreichel/alpine-typewriter'; import '../Molecule/LogoBar/LogoBar'; +import '../Molecule/Slider/Slider'; // @ts-ignore Alpine.plugin([anchor, clipboard, collapse, focus, intersect, typewriter]); diff --git a/package.json b/package.json index edd045995..083bd11e4 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,8 @@ "@floating-ui/dom": "^1.6.3", "@marcreichel/alpine-typewriter": "^1.2.0", "@ryangjchandler/alpine-clipboard": "^2.3.0", + "@splidejs/splide": "^4.1.4", + "@splidejs/splide-extension-video": "^0.8.0", "alpinejs": "^3.13.5" } }