From c09cf6e3d545e729c37c693c443ec73aff00485c Mon Sep 17 00:00:00 2001 From: Shivangi Singh Date: Thu, 21 Sep 2023 16:23:18 +0530 Subject: [PATCH 1/3] addding a script --- blocks/carousel/carousel-cards.css | 196 +++++++++++ blocks/carousel/carousel.css | 455 ++++++++++++++++++++++++++ blocks/carousel/carousel.js | 502 +++++++++++++++++++++++++++++ scripts/dom-helpers.js | 88 +++++ 4 files changed, 1241 insertions(+) create mode 100644 blocks/carousel/carousel-cards.css create mode 100644 blocks/carousel/carousel.css create mode 100644 blocks/carousel/carousel.js create mode 100644 scripts/dom-helpers.js diff --git a/blocks/carousel/carousel-cards.css b/blocks/carousel/carousel-cards.css new file mode 100644 index 0000000..ca6c831 --- /dev/null +++ b/blocks/carousel/carousel-cards.css @@ -0,0 +1,196 @@ +/* + Cards Carousel specific styling. +*/ +.carousel-wrapper.cards { + width: 1020px; + margin: 0 auto; +} + +.block.carousel.cards.fully-visible { + justify-content: center; +} + +.block.carousel.cards { + text-align: left; +} + +.carousel-wrapper.cards .carousel-nav-button:hover { + color: #adb3b7; + border: 1.5px solid #adb3b7; + background-color: inherit; +} + +.carousel-wrapper.cards .carousel-nav-left { + left: -50px; +} + +.carousel-wrapper.cards .carousel-nav-right { + right: -50px; +} + +.carousel.cards { + padding: 0; +} + +.carousel.cards .carousel-item { + display: block; + width: 33.3%; + max-width: 33.3%; + padding: 20px 15px; + margin: 0; +} + +.carousel.cards .carousel-item:hover { + z-index: 1; + transform: scale(1.01); + transition: all 0.3s; +} + +.carousel.cards .carousel-item .card { + height: 100%; + background: #fff; + margin: 0; + position: relative; + box-shadow: 0 1px 15px rgba(0 0 0 / 40%); + transition: all 0.3s; + display: flex; + align-items: stretch; + align-content: stretch; + width: 100%; + flex-direction: column; + justify-content: space-between; + margin-bottom: 30px; +} + +.carousel.cards .carousel-item h3 { + font-size: var(--heading-font-size-l); + margin-bottom: 8px; + margin-top: 0; + color: var(--text-color); +} + +.carousel.cards .carousel-item p { + font-size: var(--body-font-size-xs); + margin: 0; + color: var(--text-color); + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; +} + +.carousel.cards.gpx-compliance-resources .carousel-item p:first-child { + text-transform: uppercase; + color: #6eb43f; + font-size: 14px; + margin-bottom: 10px; +} + +.carousel.cards .carousel-item .button-container { + margin-top: auto; + margin-bottom: 0; +} + +.carousel.cards .carousel-item a { + font-size: 18px; + color: #008ca8; +} + +.carousel.cards .carousel-item a span { + margin-left: 5px; + vertical-align: text-top; +} + +.carousel.cards .carousel-item a span svg { + height: 18px; + width: 18px; +} + +.carousel.cards .carousel-item .card > div:last-child { + display: flex; + flex-direction: column; + padding: 10px 20px; + color: #12141f; + height: 100%; + min-height: auto; +} + +.carousel.cards.gpx-compliance-resources .carousel-item .card > div:first-child, +.carousel.cards.microplate-related-products .carousel-item .card > div:first-child { + flex-shrink: 0; +} + +.carousel.cards.gpx-compliance-resources .carousel-item .card > div:first-child { + height: 200px; +} + +.carousel.cards.microplate-related-products .carousel-item .card > div:first-child { + height: 250px; +} + +.carousel.cards.gpx-compliance-resources .carousel-item .card > div:first-child picture, +.carousel.cards.microplate-related-products .carousel-item .card > div:first-child picture { + display: block; + height: 100%; +} + +.carousel.cards.gpx-compliance-resources .carousel-item .card > div:first-child img, +.carousel.cards.microplate-related-products .carousel-item .card > div:first-child img { + height: 100%; + width: 100%; + object-fit: cover; +} + +.carousel.cards.gpx-compliance-resources .carousel-item .card > div:last-child { + height: calc(100% - 200px); +} + +.carousel.cards.microplate-related-products .carousel-item .card > div:last-child { + height: calc(100% - 250px); +} + +.carousel.cards .carousel-item-columns-container { + flex-direction: column; +} + +@media only screen and (max-width: 1199px) { + .carousel-wrapper.cards { + width: 940px; + } +} + +@media only screen and (max-width: 991px) { + .carousel-wrapper.cards { + width: 750px; + } + + .carousel-wrapper.cards .carousel-item { + width: 50%; + max-width: 50%; + } +} + +@media only screen and (max-width: 767px) { + .carousel-wrapper.cards { + width: 100%; + } + + .carousel.cards { + width: 100%; + padding: 0 10%; + } + + .carousel-wrapper.cards .carousel-nav-button { + display: none; + } + + .carousel-wrapper.cards .carousel-item { + width: 100%; + max-width: 100%; + } + + .carousel.cards .carousel-item img { + width: 100%; + } +} diff --git a/blocks/carousel/carousel.css b/blocks/carousel/carousel.css new file mode 100644 index 0000000..25ed7f4 --- /dev/null +++ b/blocks/carousel/carousel.css @@ -0,0 +1,455 @@ +/* + Default Carousel Styling. + Same as .products on the landing page. + Variant stylings can be found below. +*/ + +main div.carousel-wrapper { + width: 100%; + position: relative; + max-width: unset; +} + +main div.carousel-wrapper:last-child { + margin-bottom: 0 !important; +} + +main .carousel { + box-sizing: border-box; + display: flex; + overflow: hidden; + margin: auto; + width: 100%; /* default - same as .products */ + padding-left: 200px; /* default - same as .products */ + padding-right: 200px; /* default - same as .products */ +} + +main .carousel-item { + box-sizing: border-box; + width: 100%; + margin-right: 30px; + flex: 1 0 auto; + display: flex; + align-items: center; + justify-content: center; + padding: 50px 80px; /* default - same as .products */ +} + +main .carousel-item-columns-container { + max-width: 1140px; + display: flex; + align-items: center; + justify-content: center; +} + +main .carousel-item-column { + width: 50%; + padding: 0 15px; +} + +main .carousel-item-image img { + display: block; + object-fit: contain; + width: 100%; /* default - same as .products */ + max-height: 400px; /* default - same as .products */ +} + +main .carousel-item-text h3 { + font-size: 32px; + font-weight: lighter; +} + +main .carousel-item-text .button-container { + display: inline-block; +} + +main .carousel-item-text .button-container .secondary { + margin-left: 15px; +} + +main .carousel-nav-button { + z-index: 99; + min-width: unset; + padding: 0; + border-radius: 50%; + position: absolute; + top: 50%; + transform: translate(0, -50%); + display: flex; + justify-content: center; + align-items: center; + color: #adb3b7; /* default - same as .products */ + border: 1.5px solid #adb3b7; /* default - same as .products */ + background-color: inherit; /* default - same as .products */ +} + +main .carousel-nav-button.disabled { + display: none; +} + +main .carousel-nav-button:hover { + background-color: #adb3b7; + color: var(--text-white); + border: 1.5px solid var(--background-color); +} + +main .carousel-nav-button .icon { + height: 38px; + width: 38px; + stroke-width: 20; +} + +main .carousel-nav-left { + left: 20px; +} + +main .carousel-nav-right { + right: 20px; +} + +main .carousel-dot-buttons { + display: flex; + justify-content: center; + gap: 10px; + min-width: unset; + margin-top: 20px; +} + +main .carousel-dot-button { + min-width: unset; + height: 20px; + width: 20px; + background-color: transparent; + border: 1px solid #cacaca; + border-radius: 50%; + padding: 0; +} + +main .carousel-dot-button.selected { + background-color: #cacaca; + border: 0; +} + +@media only screen and (max-width: 1400px) { + main .carousel { + padding-left: 100px; + padding-right: 100px; + } +} + +@media only screen and (max-width: 991px) { + main .carousel-item-columns-container { + flex-direction: column; + gap: 30px; + } + + main .carousel-item-column { + width: 100%; + padding: 0; + align-items: center; + } + + main .carousel-item-image img { + max-height: 300px; + } +} + +@media only screen and (max-width: 767px) { + main .carousel { + padding-left: 40px; + padding-right: 40px; + } + + main .carousel-item { + padding: 20px; + } + + main .carousel-item-text h3 { + font-size: 24px; + } + + main .carousel-item-text .button-container { + margin-top: 30px; + max-width: 200px !important; + width: 100% !important; + } + + main .carousel-item-text .button-container .button { + max-width: 200px !important; + width: 100% !important; + font-size: 12px; + } + + main .carousel-nav-button .icon { + height: 26px; + width: 26px; + } + + main .carousel-nav-button { + width: unset; + } +} + +@media only screen and (max-width: 576px) { + main .carousel-item-text .button-container { + margin-bottom: 0; + } + + main .carousel-item-text .button-container .button { + margin-left: 0; + } +} + +@media only screen and (max-width: 420px) { + main .carousel-item-text .button-container { + max-width: unset !important; + display: block; + } + + main .carousel-item-text .button-container .button { + max-width: unset !important; + } + + main .carousel a.button { + display: block; + } +} + +/* WAVE carousel variant */ +main div.carousel-wrapper.wave { + width: 1170px; +} + +main .carousel.wave { + padding: 0; +} + +main .carousel.wave .carousel-item-columns-container { + gap: 2em; +} + +main .carousel.wave .carousel-item { + padding: 20px 15px; + height: 100%; +} + +main .carousel.wave .carousel-item-column { + width: unset; + padding: 0; +} + +main .carousel.wave .carousel-item-image img { + height: 353px; + width: 330px; +} + +main .carousel.wave .carousel-item-text { + margin-top: 13%; + max-width: 597px; + padding-right: 15px; + text-align: center; + position: relative; +} + +main .carousel.wave .carousel-item-text p { + color: var(--text-white); + font-size: 27px; +} + +main .carousel.wave .carousel-item-text .button-container a.secondary, +main .carousel.wave .carousel-item-text .button-container a.secondary:hover { + background-color: transparent; + color: var(--text-white); + border: 1.5px solid var(--text-white); +} + +main .carousel-wrapper.wave .carousel-dot-buttons { + margin-top: 0; +} + +main .carousel-wrapper.wave .carousel-dot-button { + background-color: var(--background-color); +} + +main .carousel-wrapper.wave .carousel-nav-button:hover { + color: #adb3b7; + border: 1.5px solid #adb3b7; + background-color: inherit; +} + +@media only screen and (max-width: 1200px) and (min-width: 992px) { + main div.carousel-wrapper.wave { + width: 970px; + } + + main .carousel.wave { + max-width: 90%; + height: auto; + margin: auto; + } + + main .carousel.wave .carousel-item-columns-container { + gap: 1em; + } + + main .carousel.wave .carousel-item-text { + max-width: 530px; + } + + main .carousel.wave .carousel-item-image img { + height: 310px; + width: 200px; + } +} + +@media only screen and (max-width: 992px) { + main div.carousel-wrapper.wave { + width: 750px; + } + + main .carousel.wave .carousel-item-image img { + display: none; + } +} + +@media only screen and (max-width: 767px) { + main div.carousel-wrapper.wave { + width: 100%; + } + + main .carousel.wave { + /* + Ugly hack because you cannot mix overflow-x: hidden with overflow-y: visible + https://stackoverflow.com/a/39554003 + */ + padding-top: 170px; + margin-top: -170px; + } + + main .carousel.wave .carousel-item { + position: relative; + padding: 0 30px; + max-width: 90%; + } + + main .carousel.wave .carousel-item-columns-container { + gap: 0; + } + + main .carousel.wave .carousel-item-image img { + max-width: unset; + display: block; + height: 290px; + width: 260px; + flex-shrink: 0; + } + + main .carousel.wave .carousel-item-image { + position: absolute; + left: 50%; + transform: translate(-50%, 0); + margin: auto; + display: block; + top: -180px; + } + + main .carousel.wave .carousel-item-text { + margin-top: 140px; + padding-right: 0; + padding-left: 0; + padding-bottom: 25px; + max-width: unset; + } + + main .carousel-wrapper.wave .carousel-dot-buttons { + margin-bottom: 25px; + } +} + +/* WAVE Blue Variant */ +main div.carousel-wrapper.wave.blue .carousel-dot-button.selected { + background-color: #23bfd9; +} + +/* WAVE BlueGreen Variant */ +main div.carousel-wrapper.wave.bluegreen .carousel-dot-button.selected { + background-color: #86c440; +} + +main .carousel .carousel-item-text blockquote { + margin: 0; + position: static; +} + +main .carousel .carousel-item .carousel-item-text blockquote ~ p, +main .carousel .carousel-item .carousel-item-text blockquote > p { + padding: 0; + font-style: normal; + font-size: 23px; + line-height: 28px; +} + +main .carousel .carousel-item .carousel-item-text blockquote > p::before, +main .carousel .carousel-item .carousel-item-text blockquote > p::after { + content: none; +} + +main .carousel blockquote::before, +main .carousel blockquote::after { + content: ""; + position: absolute; + width: 100px; + height: 100px; + background-position: center; + background-size: 100px; + background-repeat: no-repeat; +} + +main .carousel blockquote::before { + background-image: url('/images/quote-left.png'); + left: -50px; + top: -89px; +} + +main .carousel blockquote::after { + background-image: url('/images/quote-right.png'); + right: -1.5em; + bottom: -60px; +} + +@media only screen and (max-width: 992px) { + main .carousel blockquote::before, + main .carousel blockquote::after { + width: 33px; + height: 33px; + background-size: 33px; + } + + main .carousel blockquote::before { + left: -8px; + top: -35px; + } + + main .carousel blockquote::after { + right: 0; + bottom: 0; + } +} + +@media only screen and (max-width: 767px) { + main .carousel .carousel-item .carousel-item-text blockquote > p { + font-size: 16px; + } + + main .carousel .carousel-item .carousel-item-text blockquote + p { + font-size: 18px; + } +} + +main .block.carousel.full-size .carousel-item-column { + width: auto; +} + +main .block.carousel.full-size .carousel-item-image img { + max-height: none; +} diff --git a/blocks/carousel/carousel.js b/blocks/carousel/carousel.js new file mode 100644 index 0000000..7ffb9bb --- /dev/null +++ b/blocks/carousel/carousel.js @@ -0,0 +1,502 @@ +/* eslint-disable no-unused-expressions */ +import { decorateIcons, loadCSS } from '../../scripts/lib-franklin.js'; +import { div, p, span } from '../../scripts/dom-helpers.js'; +import { handleCompareProducts } from '../card/card.js'; + +const AUTOSCROLL_INTERVAL = 7000; + +/** + * Clone a carousel item + * @param {Element} item carousel item to be cloned + * @returns the clone of the carousel item + */ +function createClone(item) { + const clone = item.cloneNode(true); + clone.classList.add('clone'); + clone.classList.remove('selected'); + + // if clone has compare box, add event handler + const compareCheckbox = clone.querySelector('.compare-checkbox'); + if (compareCheckbox) { + compareCheckbox.addEventListener('click', handleCompareProducts); + } + + return clone; +} + +class Carousel { + constructor(block, data, config) { + // Set defaults + this.cssFiles = []; + this.defaultStyling = false; + this.dotButtons = true; + this.navButtons = true; + this.counter = false; + this.infiniteScroll = true; + this.autoScroll = true; // only available with infinite scroll + this.autoScrollInterval = AUTOSCROLL_INTERVAL; + this.currentIndex = 0; + this.counterText = ''; + this.counterNavButtons = true; + this.cardRenderer = this; + // this is primarily controlled by CSS, + // but we need to know then intention for scrolling pourposes + this.visibleItems = [ + { + items: 1, + condition: () => true, + }, + ]; + + // Set information + this.block = block; + this.data = data || [...block.children]; + + // Will be replaced after rendering, if available + this.navButtonLeft = null; + this.navButtonRight = null; + + // Apply overwrites + Object.assign(this, config); + + if (this.getCurrentVisibleItems() >= this.data.length) { + this.infiniteScroll = false; + this.navButtons = false; + this.block.classList.add('fully-visible'); + } + + if (this.defaultStyling) { + this.cssFiles.push('/blocks/carousel/carousel.css'); + } + } + + getBlockPadding() { + if (!this.blockStyle) { + this.blockStyle = window.getComputedStyle(this.block); + } + return +(this.blockStyle.getPropertyValue('padding-left').replace('px', '')); + } + + updateCounterText(newIndex = this.currentIndex) { + this.currentIndex = newIndex; + if (this.counter) { + const items = this.block.querySelectorAll('.carousel-item:not(.clone,.skip)'); + const counterTextBlock = this.block.parentElement.querySelector('.carousel-counter .carousel-counter-text p'); + const pageCounter = `${this.currentIndex + 1} / ${items.length}`; + counterTextBlock.innerHTML = this.counterText ? `${this.counterText} ${pageCounter}` : pageCounter; + } + } + + /** + * Scroll the carousel to the next item + */ + nextItem() { + !this.infiniteScroll && this.navButtonRight && this.navButtonRight.classList.remove('disabled'); + !this.infiniteScroll && this.navButtonLeft && this.navButtonLeft.classList.remove('disabled'); + + const dotButtons = this.block.parentNode.querySelectorAll('.carousel-dot-button'); + const items = this.block.querySelectorAll('.carousel-item:not(.clone,.skip)'); + const selectedItem = this.block.querySelector('.carousel-item.selected'); + + let index = [...items].indexOf(selectedItem); + index = index !== -1 ? index : 0; + + const newIndex = (index + 1) % items.length; + const newSelectedItem = items[newIndex]; + if (newIndex === 0 && !this.infiniteScroll) { + return; + } + + if (newIndex === items.length - this.getCurrentVisibleItems() && !this.infiniteScroll) { + this.navButtonRight.classList.add('disabled'); + } + + if (newIndex === 0) { + // create the ilusion of infinite scrolling + newSelectedItem.parentNode.scrollTo({ + top: 0, + left: ( + newSelectedItem.previousElementSibling.offsetLeft + - this.getBlockPadding() + - this.block.offsetLeft + ), + }); + } + + newSelectedItem.parentNode.scrollTo({ + top: 0, + left: newSelectedItem.offsetLeft - this.getBlockPadding() - this.block.offsetLeft, + behavior: 'smooth', + }); + + items.forEach((item) => item.classList.remove('selected')); + dotButtons.forEach((item) => item.classList.remove('selected')); + newSelectedItem.classList.add('selected'); + if (dotButtons && dotButtons.length !== 0) { + dotButtons[newIndex].classList.add('selected'); + } + + this.updateCounterText(newIndex); + } + + getCurrentVisibleItems() { + return this.visibleItems + .filter((e) => !e.condition || e.condition())[0].items; + } + + /** + * Scroll the carousel to the previous item + */ + prevItem() { + !this.infiniteScroll && this.navButtonRight && this.navButtonRight.classList.remove('disabled'); + !this.infiniteScroll && this.navButtonLeft && this.navButtonLeft.classList.remove('disabled'); + + const dotButtons = this.block.parentNode.querySelectorAll('.carousel-dot-button'); + const items = this.block.querySelectorAll('.carousel-item:not(.clone,.skip)'); + const selectedItem = this.block.querySelector('.carousel-item.selected'); + + let index = [...items].indexOf(selectedItem); + index = index !== -1 ? index : 0; + const newIndex = index - 1 < 0 ? items.length - 1 : index - 1; + const newSelectedItem = items[newIndex]; + + if (newIndex === items.length - 1 && !this.infiniteScroll) { + return; + } + + if (newIndex === 0 && !this.infiniteScroll) { + this.navButtonLeft.classList.add('disabled'); + } + + if (newIndex === items.length - 1) { + // create the ilusion of infinite scrolling + newSelectedItem.parentNode.scrollTo({ + top: 0, + left: ( + newSelectedItem.nextElementSibling.offsetLeft + - this.getBlockPadding() + - this.block.offsetLeft + ), + }); + } + + newSelectedItem.parentNode.scrollTo({ + top: 0, + left: newSelectedItem.offsetLeft - this.getBlockPadding() - this.block.offsetLeft, + behavior: 'smooth', + }); + + items.forEach((item) => item.classList.remove('selected')); + dotButtons.forEach((item) => item.classList.remove('selected')); + newSelectedItem.classList.add('selected'); + if (dotButtons && dotButtons.length !== 0) { + dotButtons[newIndex].classList.add('selected'); + } + + this.updateCounterText(newIndex); + } + + /** + * Create clone items at the beginning and end of the carousel + * to give the appearance of infinite scrolling + */ + createClones() { + if (this.block.children.length < 2) return; + + const initialChildren = [...this.block.children]; + + this.block.lastChild.after(createClone(initialChildren[0])); + this.block.lastChild.after(createClone(initialChildren[1])); + + this.block.firstChild.before(createClone(initialChildren[initialChildren.length - 1])); + this.block.firstChild.before(createClone(initialChildren[initialChildren.length - 2])); + } + + /** + * Create left and right arrow navigation buttons + */ + createNavButtons(parentElement) { + const buttonLeft = document.createElement('button'); + buttonLeft.classList.add('carousel-nav-left'); + buttonLeft.ariaLabel = 'Scroll to previous item'; + buttonLeft.append(span({ class: 'icon icon-chevron-left' })); + buttonLeft.addEventListener('click', () => { + clearInterval(this.intervalId); + this.prevItem(); + }); + + if (!this.infiniteScroll) { + buttonLeft.classList.add('disabled'); + } + + const buttonRight = document.createElement('button'); + buttonRight.classList.add('carousel-nav-right'); + buttonRight.ariaLabel = 'Scroll to next item'; + buttonRight.append(span({ class: 'icon icon-chevron-right' })); + buttonRight.addEventListener('click', () => { + clearInterval(this.intervalId); + this.nextItem(); + }); + + [buttonLeft, buttonRight].forEach((navButton) => { + navButton.classList.add('carousel-nav-button'); + parentElement.append(navButton); + }); + + decorateIcons(buttonLeft); + decorateIcons(buttonRight); + this.navButtonLeft = buttonLeft; + this.navButtonRight = buttonRight; + } + + /** + * Adds event listeners for touch UI swiping + */ + addSwipeCapability() { + if (this.block.swipeCapabilityAdded) { + return; + } + + let touchstartX = 0; + let touchendX = 0; + + this.block.addEventListener('touchstart', (e) => { + touchstartX = e.changedTouches[0].screenX; + }, { passive: true }); + + this.block.addEventListener('touchend', (e) => { + touchendX = e.changedTouches[0].screenX; + if (Math.abs(touchendX - touchstartX) < 10) { + return; + } + + if (touchendX < touchstartX) { + clearInterval(this.intervalId); + this.nextItem(); + } + + if (touchendX > touchstartX) { + clearInterval(this.intervalId); + this.prevItem(); + } + }, { passive: true }); + this.block.swipeCapabilityAdded = true; + } + + setInitialScrollingPosition() { + const scrollToSelectedItem = () => { + const item = this.block.querySelector('.carousel-item.selected'); + item.parentNode.scrollTo({ + top: 0, + left: item.offsetLeft - this.getBlockPadding() - this.block.offsetLeft, + }); + }; + + const section = this.block.closest('.section'); + + const observer = new MutationObserver((mutationList) => { + mutationList.forEach((mutation) => { + if (mutation.type === 'attributes' + && mutation.attributeName === 'data-section-status' + && section.attributes.getNamedItem('data-section-status').value === 'loaded') { + scrollToSelectedItem(); + observer.disconnect(); + } + }); + }); + + observer.observe(section, { attributes: true }); + + // just in case the mutation observer didn't work + setTimeout(scrollToSelectedItem, 700); + + // ensure that we disconnect the observer + // if the animation has kicked in, we for sure no longer need it + setTimeout(() => { observer.disconnect(); }, AUTOSCROLL_INTERVAL); + } + + createDotButtons() { + const buttons = document.createElement('div'); + buttons.className = 'carousel-dot-buttons'; + const items = [...this.block.children]; + + items.forEach((item, i) => { + const button = document.createElement('button'); + button.ariaLabel = `Scroll to item ${i + 1}`; + button.classList.add('carousel-dot-button'); + if (i === this.currentIndex) { + button.classList.add('selected'); + } + + button.addEventListener('click', () => { + clearInterval(this.intervalId); + this.block.scrollTo({ + top: 0, + left: item.offsetLeft - this.getBlockPadding(), + behavior: 'smooth', + }); + [...buttons.children].forEach((r) => r.classList.remove('selected')); + items.forEach((r) => r.classList.remove('selected')); + button.classList.add('selected'); + item.classList.add('selected'); + this.updateCounterText(i); + }); + buttons.append(button); + }); + this.block.parentElement.append(buttons); + } + + createCounter() { + const counter = div({ class: 'carousel-counter' }, + div({ class: 'carousel-counter-text' }, + p(''), + ), + ); + if (this.counterNavButtons) { + this.createNavButtons(counter); + } + this.block.parentElement.append(counter); + this.updateCounterText(); + } + + /* + * Changing the default rendering may break carousels that rely on it + * (e.g. CSS might not match anymore) + */ + // eslint-disable-next-line class-methods-use-this + renderItem(item) { + // create the carousel content + const columnContainer = document.createElement('div'); + columnContainer.classList.add('carousel-item-columns-container'); + + const columns = [document.createElement('div'), document.createElement('div')]; + + const itemChildren = [...item.children]; + itemChildren.forEach((itemChild, idx) => { + if (itemChild.querySelector('img')) { + itemChild.classList.add('carousel-item-image'); + } else { + itemChild.classList.add('carousel-item-text'); + } + columns[idx].appendChild(itemChild); + }); + + columns.forEach((column) => { + column.classList.add('carousel-item-column'); + columnContainer.appendChild(column); + }); + return columnContainer; + } + + async render() { + // copy carousel styles to the wrapper too + this.block.parentElement.classList.add( + ...[...this.block.classList].filter((item, idx) => idx !== 0 && item !== 'block'), + ); + + let defaultCSSPromise; + if (Array.isArray(this.cssFiles) && this.cssFiles.length > 0) { + // add default carousel classes to apply default CSS + defaultCSSPromise = new Promise((resolve) => { + this.cssFiles.forEach((cssFile) => { + loadCSS(cssFile, (e) => resolve(e)); + }); + }); + this.block.parentElement.classList.add('carousel-wrapper'); + this.block.classList.add('carousel'); + } + + this.block.innerHTML = ''; + this.data.forEach((item, index) => { + const itemContainer = document.createElement('div'); + itemContainer.classList.add('carousel-item', `carousel-item-${index + 1}`); + + let renderedItem = this.cardRenderer.renderItem(item); + renderedItem = Array.isArray(renderedItem) ? renderedItem : [renderedItem]; + renderedItem.forEach((renderedItemElement) => { + // There may be items in the carousel that are skipped from scrolling + if (renderedItemElement.classList.contains('carousel-skip-item')) { + itemContainer.classList.add('skip'); + } + itemContainer.appendChild(renderedItemElement); + }); + this.block.appendChild(itemContainer); + }); + + // set initial selected carousel item + const activeItems = this.block.querySelectorAll('.carousel-item:not(.clone,.skip)'); + activeItems[this.currentIndex].classList.add('selected'); + + // create autoscrolling animation + this.autoScroll && this.infiniteScroll + && (this.intervalId = setInterval(() => { this.nextItem(); }, this.autoScrollInterval)); + this.dotButtons && this.createDotButtons(); + this.counter && this.createCounter(); + this.navButtons && this.createNavButtons(this.block.parentElement); + this.infiniteScroll && this.createClones(); + this.addSwipeCapability(); + this.infiniteScroll && this.setInitialScrollingPosition(); + this.cssFiles && (await defaultCSSPromise); + } +} + +/** + * Create and render default carousel. + * Best practice: Create a new block and call the function, instead using or modifying this. + * @param {Element} block required - target block + * @param {Array} data optional - a list of data elements. + * either a list of objects or a list of divs. + * if not provided: the div children of the block are used + * @param {Object} config optional - config object for + * customizing the rendering and behaviour + */ +export async function createCarousel(block, data, config) { + const carousel = new Carousel(block, data, config); + await carousel.render(); + return carousel; +} + +/** + * Custom card style config and rendering of carousel items. + */ +export function renderCardItem(item) { + item.classList.add('card'); + item + .querySelectorAll('.button-container a') + .forEach((a) => a.append(span({ class: 'icon icon-chevron-right-outline', 'aria-hidden': true }))); + decorateIcons(item); + return item; +} + +const cardStyleConfig = { + cssFiles: ['/blocks/carousel/carousel-cards.css'], + navButtons: true, + dotButtons: false, + infiniteScroll: true, + autoScroll: false, + visibleItems: [ + { + items: 1, + condition: () => window.innerWidth < 768, + }, + { + items: 2, + condition: () => window.innerWidth < 1200, + }, { + items: 3, + }, + ], + renderItem: renderCardItem, +}; + +export default async function decorate(block) { + // cards style carousel + const useCardsStyle = block.classList.contains('cards'); + if (useCardsStyle) { + await createCarousel(block, [...block.children], cardStyleConfig); + return; + } + + // use the default carousel + await createCarousel(block); +} diff --git a/scripts/dom-helpers.js b/scripts/dom-helpers.js new file mode 100644 index 0000000..da289c3 --- /dev/null +++ b/scripts/dom-helpers.js @@ -0,0 +1,88 @@ +/* eslint-disable no-param-reassign */ + +/** + * Example Usage: + * + * domEl('main', + * div({ class: 'card' }, + * a({ href: item.path }, + * div({ class: 'card-thumb' }, + * createOptimizedPicture(item.image, item.title, 'lazy', [{ width: '800' }]), + * ), + * div({ class: 'card-caption' }, + * h3(item.title), + * p({ class: 'card-description' }, item.description), + * p({ class: 'button-container' }, + * a({ href: item.path, 'aria-label': 'Read More', class: 'button primary' }, 'Read More'), + * ), + * ), + * ), + * ) + */ + +/** + * Helper for more concisely generating DOM Elements with attributes and children + * @param {string} tag HTML tag of the desired element + * @param {[Object?, ...Element]} items: First item can optionally be an object of attributes, + * everything else is a child element + * @returns {Element} The constructred DOM Element + */ +export function domEl(tag, ...items) { + const element = document.createElement(tag); + + if (!items || items.length === 0) return element; + + if (!(items[0] instanceof Element || items[0] instanceof HTMLElement) && typeof items[0] === 'object') { + const [attributes, ...rest] = items; + items = rest; + + Object.entries(attributes).forEach(([key, value]) => { + if (!key.startsWith('on')) { + element.setAttribute(key, Array.isArray(value) ? value.join(' ') : value); + } else { + element.addEventListener(key.substring(2).toLowerCase(), value); + } + }); + } + + items.forEach((item) => { + item = item instanceof Element || item instanceof HTMLElement + ? item + : document.createTextNode(item); + element.appendChild(item); + }); + + return element; + } + + /* + More short hand functions can be added for very common DOM elements below. + domEl function from above can be used for one off DOM element occurrences. + */ + export function div(...items) { return domEl('div', ...items); } + export function p(...items) { return domEl('p', ...items); } + export function a(...items) { return domEl('a', ...items); } + export function h1(...items) { return domEl('h1', ...items); } + export function h2(...items) { return domEl('h2', ...items); } + export function h3(...items) { return domEl('h3', ...items); } + export function h4(...items) { return domEl('h4', ...items); } + export function h5(...items) { return domEl('h5', ...items); } + export function h6(...items) { return domEl('h6', ...items); } + export function ul(...items) { return domEl('ul', ...items); } + export function ol(...items) { return domEl('ol', ...items); } + export function li(...items) { return domEl('li', ...items); } + export function i(...items) { return domEl('i', ...items); } + export function img(...items) { return domEl('img', ...items); } + export function span(...items) { return domEl('span', ...items); } + export function form(...items) { return domEl('form', ...items); } + export function input(...items) { return domEl('input', ...items); } + export function label(...items) { return domEl('label', ...items); } + export function button(...items) { return domEl('button', ...items); } + export function iframe(...items) { return domEl('iframe', ...items); } + export function nav(...items) { return domEl('nav', ...items); } + export function fieldset(...items) { return domEl('fieldset', ...items); } + export function article(...items) { return domEl('article', ...items); } + export function strong(...items) { return domEl('strong', ...items); } + export function select(...items) { return domEl('select', ...items); } + export function option(...items) { return domEl('option', ...items); } + \ No newline at end of file From 17905dc29812fc99d4152c19ec289a0f52d2c88c Mon Sep 17 00:00:00 2001 From: Shivangi Singh Date: Fri, 22 Sep 2023 16:19:31 +0530 Subject: [PATCH 2/3] carousel --- blocks/carousel/carousel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blocks/carousel/carousel.js b/blocks/carousel/carousel.js index 7ffb9bb..0516c54 100644 --- a/blocks/carousel/carousel.js +++ b/blocks/carousel/carousel.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-expressions */ import { decorateIcons, loadCSS } from '../../scripts/lib-franklin.js'; import { div, p, span } from '../../scripts/dom-helpers.js'; -import { handleCompareProducts } from '../card/card.js'; +import { handleCompareProducts } from '../cards/cards.js'; const AUTOSCROLL_INTERVAL = 7000; From aa6a733bf54dac9c98678182674aba20d0d6ad6f Mon Sep 17 00:00:00 2001 From: Shivangi Singh Date: Fri, 22 Sep 2023 17:10:28 +0530 Subject: [PATCH 3/3] Carousal changes --- blocks/carousel/carousel.css | 58 +++--------------------------------- blocks/carousel/carousel.js | 40 +------------------------ 2 files changed, 5 insertions(+), 93 deletions(-) diff --git a/blocks/carousel/carousel.css b/blocks/carousel/carousel.css index 25ed7f4..b484a8e 100644 --- a/blocks/carousel/carousel.css +++ b/blocks/carousel/carousel.css @@ -43,8 +43,7 @@ main .carousel-item-columns-container { } main .carousel-item-column { - width: 50%; - padding: 0 15px; + width: auto; } main .carousel-item-image img { @@ -67,46 +66,6 @@ main .carousel-item-text .button-container .secondary { margin-left: 15px; } -main .carousel-nav-button { - z-index: 99; - min-width: unset; - padding: 0; - border-radius: 50%; - position: absolute; - top: 50%; - transform: translate(0, -50%); - display: flex; - justify-content: center; - align-items: center; - color: #adb3b7; /* default - same as .products */ - border: 1.5px solid #adb3b7; /* default - same as .products */ - background-color: inherit; /* default - same as .products */ -} - -main .carousel-nav-button.disabled { - display: none; -} - -main .carousel-nav-button:hover { - background-color: #adb3b7; - color: var(--text-white); - border: 1.5px solid var(--background-color); -} - -main .carousel-nav-button .icon { - height: 38px; - width: 38px; - stroke-width: 20; -} - -main .carousel-nav-left { - left: 20px; -} - -main .carousel-nav-right { - right: 20px; -} - main .carousel-dot-buttons { display: flex; justify-content: center; @@ -150,7 +109,7 @@ main .carousel-dot-button.selected { } main .carousel-item-image img { - max-height: 300px; + max-height: auto; } } @@ -179,15 +138,6 @@ main .carousel-dot-button.selected { width: 100% !important; font-size: 12px; } - - main .carousel-nav-button .icon { - height: 26px; - width: 26px; - } - - main .carousel-nav-button { - width: unset; - } } @media only screen and (max-width: 576px) { @@ -239,8 +189,8 @@ main .carousel.wave .carousel-item-column { } main .carousel.wave .carousel-item-image img { - height: 353px; - width: 330px; + height: auto; + width: auto; } main .carousel.wave .carousel-item-text { diff --git a/blocks/carousel/carousel.js b/blocks/carousel/carousel.js index 0516c54..127afdc 100644 --- a/blocks/carousel/carousel.js +++ b/blocks/carousel/carousel.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-expressions */ import { decorateIcons, loadCSS } from '../../scripts/lib-franklin.js'; import { div, p, span } from '../../scripts/dom-helpers.js'; -import { handleCompareProducts } from '../cards/cards.js'; +import { handleCompareProducts } from '../card/card.js'; const AUTOSCROLL_INTERVAL = 7000; @@ -212,43 +212,6 @@ class Carousel { this.block.firstChild.before(createClone(initialChildren[initialChildren.length - 2])); } - /** - * Create left and right arrow navigation buttons - */ - createNavButtons(parentElement) { - const buttonLeft = document.createElement('button'); - buttonLeft.classList.add('carousel-nav-left'); - buttonLeft.ariaLabel = 'Scroll to previous item'; - buttonLeft.append(span({ class: 'icon icon-chevron-left' })); - buttonLeft.addEventListener('click', () => { - clearInterval(this.intervalId); - this.prevItem(); - }); - - if (!this.infiniteScroll) { - buttonLeft.classList.add('disabled'); - } - - const buttonRight = document.createElement('button'); - buttonRight.classList.add('carousel-nav-right'); - buttonRight.ariaLabel = 'Scroll to next item'; - buttonRight.append(span({ class: 'icon icon-chevron-right' })); - buttonRight.addEventListener('click', () => { - clearInterval(this.intervalId); - this.nextItem(); - }); - - [buttonLeft, buttonRight].forEach((navButton) => { - navButton.classList.add('carousel-nav-button'); - parentElement.append(navButton); - }); - - decorateIcons(buttonLeft); - decorateIcons(buttonRight); - this.navButtonLeft = buttonLeft; - this.navButtonRight = buttonRight; - } - /** * Adds event listeners for touch UI swiping */ @@ -432,7 +395,6 @@ class Carousel { && (this.intervalId = setInterval(() => { this.nextItem(); }, this.autoScrollInterval)); this.dotButtons && this.createDotButtons(); this.counter && this.createCounter(); - this.navButtons && this.createNavButtons(this.block.parentElement); this.infiniteScroll && this.createClones(); this.addSwipeCapability(); this.infiniteScroll && this.setInitialScrollingPosition();