]> xenbits.xensource.com Git - www-xenproject-org.git/commitdiff
Fix carousel
authorArnaud Guéras <arnaudgs@gmail.com>
Sun, 24 Nov 2024 11:15:09 +0000 (12:15 +0100)
committerArnaud Guéras <arnaudgs@gmail.com>
Sun, 24 Nov 2024 11:15:09 +0000 (12:15 +0100)
content/test.md
hugo_stats.json
themes/xen-project/assets/css/components/carousel.scss
themes/xen-project/assets/css/molecules/media-block.scss
themes/xen-project/assets/js/carousel.js
themes/xen-project/assets/js/carousel.js.bak [new file with mode: 0644]
themes/xen-project/assets/js/utils.js
themes/xen-project/layouts/partials/carousel.html

index c57a2b0f4f675f6098c7afb014e8df590b879a8c..b4ff9d7c5b7f3aeeb984a11afb6d9a786cfcb9e3 100644 (file)
@@ -3,75 +3,8 @@ title: "test"
 draft: true
 hidden: true
 ---
-
-{{<section>}}
-  {{<media-block
-    title="Lead by a **dedicated community**"
-    media="/img/flatline/team-work.svg"
-    mediaMobilePosition="bottom"
-    alt="Illustration of three people carrying a large pie chart with one segment separated, symbolizing teamwork and data analysis."
-    class="block-space"
-  >}}
-  The Xen community is a dynamic and collaborative ecosystem comprised of developers, researchers, and enthusiasts dedicated to advancing open-source virtualization technology.
-With diverse backgrounds and expertise, members actively contribute to Xen's evolution, fostering innovation, sharing knowledge, and supporting one another through continuous development efforts.
-
-  <p class="mg-t-md">
-    <a href="/contribute/get-started" class="btn btn-primary">Get started <i class="fas fa-arrow-right"></i></a>
-  </p>
-  {{</media-block>}}
-{{</section>}}
-
-{{<section>}}
-
-  {{<media-block
-    title="Lead by a **dedicated community**"
-    media="/img/flatline/team-work.svg"
-    mediaPosition="right"
-    mediaMobilePosition="bottom"
-    alt="Illustration of three people carrying a large pie chart with one segment separated, symbolizing teamwork and data analysis."
-    class="block-space"
-  >}}
-  The Xen community is a dynamic and collaborative ecosystem comprised of developers, researchers, and enthusiasts dedicated to advancing open-source virtualization technology.
-With diverse backgrounds and expertise, members actively contribute to Xen's evolution, fostering innovation, sharing knowledge, and supporting one another through continuous development efforts.
-
-  <p class="mg-t-md">
-    <a href="/contribute/get-started" class="btn btn-primary">Get started <i class="fas fa-arrow-right"></i></a>
-  </p>
-  {{</media-block>}}
-{{</section>}}
-
-
-{{<section>}}
-  {{<media-block
-    title="Why Xen Project?"
-    media=`{{<youtube id="uuBhqwbaObE" title="Xen Project's Progress Toward Safety Certification"  >}}`
-    animate="true"
-
-  >}}
-
-The Xen Project Hypervisor is uniquely placed to support a new range of use cases, building on top of 14 years of usage within the data center. In particular, its isolation and security features, flexible virtualization mode and architecture, driver disaggregation, and ARM support (only 47K lines of code) make it a perfect fit for embedded applications.
-
-{{</media-block>}}
-{{</section>}}
-
-{{<section>}}
-  {{<media-block
-    title="Basic concepts"
-    media="https://www.slideshare.net/slideshow/embed_code/key/hzJl1EbWmxfFUN"
-    alt="Slide for Unikraft's basic concepts on slideshare.net"
-    animate="true"
-  >}}
-The high-level goal of Unikraft is to be able to build unikernels targeted at specific applications without requiring the time-consuming, expert work that building such a unikernel requires today. An additional goal (or hope) of Unikraft is that all developers interested in unikernel development would contribute by supplying libraries rather than working on independent projects with different code bases as it is done now.
-{{</media-block>}}
-{{</section>}}
-
-{{<section>}}
-  {{<media-block
-    title="What is HVMI?"
-    media="https://xenproject.org/wp-content/uploads/sites/79/2020/07/github-hvmi-v2_Kek0TiK6.compressed.mp4"
-    alt="Video of a presentation about HVMI"   
-    animate="true"
-  >}}
-HVMI stands for Hypervisor-based Memory Introspection. The technology leverages Virtual Machine Introspection (VMI) APIs in the Xen and KVM hypervisors. By gaining introspection of the raw memory of running guest virtual machines, HVMI can apply security logic to detect and prevent the use of common attack techniques, such as buffer overflows, heap spray, code injection, and so-on.
-{{</media-block>}}
-{{</section>}}
+{{<section container="full">}}
+{{<carousel class="mg-t-lg carousel-container-width">}}
+{{<getpages "projects" "hidden">}}
+{{</carousel>}}
+{{</section>}}
\ No newline at end of file
index cebe3715ee1530a8ebcafda1481fc6701332fe52..9452c7f9c541290f5af065d1ee9c40e38dfc8c87 100644 (file)
@@ -78,6 +78,7 @@
       "carousel-container",
       "carousel-container-width",
       "carousel-content",
+      "carousel-content-inner",
       "carousel-item",
       "col",
       "color-txt-base",
@@ -87,7 +88,6 @@
       "container",
       "container-full",
       "content-markdown",
-      "date",
       "description",
       "download-search",
       "fa",
index 37de028a43c40fbf302912098a345b5ffac3dcee..fbe61e448e168cd685951d43098088b04eb807b7 100644 (file)
@@ -1,33 +1,37 @@
 @use "sass:math";
 
 .carousel-container {
-  --min-height: 100px;
   --content-padding: 12px;
-  --item-width: auto;
-  --item-position: 0;
   --max-width: none;
-  --number-of-items: 1;
-  --space-between-items: 40px;
-  --height: auto;
-  padding-bottom: 40px;
+  --gap: 40px;
+  --cols: 1;
+  --items-before: 1;
+  --items-after: 1;
+  --total-cols: calc(var(--cols) + var(--items-before) + var(--items-after));
+  --item-width: calc((100% - (var(--total-cols) - 1) * var(--gap)) / var(--total-cols));
+  --mask-size: var(--item-width);
+  --negative-margin-multiplier: 3;
+  --carousel-negative-margin: calc((var(--item-width) + var(--gap)) * -1 * var(--negative-margin-multiplier));
+  --color-background: var(--color-surface-secondary);
   overflow: hidden;
 
   @include phone {
-    --number-of-items: 2;
+    //--cols: 2;
   }
 
   @include tablet {
-    --number-of-items: 2;
+    --cols: 2;
     --max-width: calc(var(--container-width) + var(--content-padding) * 2);
     --content-padding: 80px;
+    --negative-margin-multiplier: 1.66;
   }
 
   @include tablet-up {
-    --number-of-items: 3;
+    --cols: 3;
   }
 
   @include desktop {
-    --number-of-items: 3;
+    --cols: 3;
   }
 
   .carousel-content {
     margin: 0 auto;
   }
 
+  .carousel-item--clone {
+    visibility: hidden;
+    pointer-events: none;
+  }
+
+  .carousel-item {
+    scroll-snap-align: start;
+
+    &--hidden {
+      pointer-events: none;
+    }
+  }
+
+  .carousel-content {
+    width: 100%;
+    position: relative;
+  }
+
+  .carousel-content-inner {
+    --opacity: 0.2;
+    --mask-image: linear-gradient(
+      to right,
+      transparent,
+      rgba(0, 0, 0, var(--opacity)) calc(var(--mask-size) - 5%),
+      black calc(var(--mask-size) + 5%),
+      black calc(100% - var(--mask-size) - 5%),
+      rgba(0, 0, 0, var(--opacity)) calc(100% - var(--mask-size) + 5%),
+      transparent
+    );
+    mask-image: var(--mask-image);
+    -webkit-mask-image: var(--mask-image);
+    margin-left: var(--carousel-negative-margin);
+    margin-right: var(--carousel-negative-margin);
+    overflow: hidden;
+  }
+
   .carousel {
     display: flex;
     flex-wrap: wrap;
-    gap: var(--space-between-items);
-    align-items: stretch;
-    position: relative;
-    height: var(--height);
+    gap: var(--gap);
+    overflow-x: scroll;
+    scroll-snap-type: x mandatory;
+    scrollbar-width: none;
+
+    &::-webkit-scrollbar {
+      display: none;
+    }
+
+    html.has-js & {
+      flex-wrap: nowrap;
+    }
 
     &-item {
-      --index: 0;
-      --transform: calc(var(--item-position) * (var(--index)));
-      flex: 1 0 calc(100% / var(--number-of-items) - var(--space-between-items));
-      top: 0;
-      min-height: var(--min-height);
-      left: 0;
       width: var(--item-width);
+      min-width: var(--item-width);
+      max-width: var(--item-width);
+      flex: 1 1 var(--item-width);
     }
   }
 
     font-size: 1.25rem;
   }
 }
-
-.carousel-initialized .carousel-item {
-  position: absolute;
-  transform: translateX(var(--transform));
-  opacity: 0;
-  display: none;
-  transition:
-    transform 0.5s ease,
-    opacity 0.8s ease;
-  height: 100%;
-}
index 1033b28ae2c61b93868caa7d6a999221d2a21218..656585847791043105c3ae890914fcfef0217bc0 100644 (file)
   &:has(video) {
     --media-h-padding: 0 !important;
   }
-  // &::before {
-  //   content: "";
-  //   position: absolute;
-  //   top: 0;
-  //   left: 50%;
-  //   width: 1px;
-  //   height: 100%;
-  //   background-color: red;
-  //   transform: translateX(-50%);
-  // }
 
   &.image-small {
     .media-block__media {
index d6c59ea096a6e0f49c3694fbb2ac69a6703ed6da..ddef7772f49ea7e0eeb6368967beb41a80824bbf 100644 (file)
 (() => {
-  const /* The `selector` constant is storing a CSS selector string that is used to select elements in
-  the DOM. In this code snippet, the `selector` constant is set to `".carousel-container"`,
-  which means it is targeting elements with the class name "carousel-container". This selector
-  is then used to find and initialize carousel functionality on those elements in the
-  document. */
-    selector = ".carousel-container";
+  document.documentElement.classList.add("has-js");
+  const selector = ".carousel-container";
+  const itemsContainerSelector = ".carousel";
   const itemSelector = ".carousel-item";
-  const itemsBefore = 2;
-  const itemsAfter = 1;
 
   const { debounce } = window.XenSiteUtils;
 
   const carousel = async (element) => {
-    const uniqueid = Math.random().toString(36).substring(2, 15);
-    element.classList.add("carousel-container-" + uniqueid);
-    const infiniteLoop = true;
-    const itemsBefore = 2;
-    const itemsAfter = 2;
-
-    const carouselElement = element.querySelector(".carousel");
-    let prev = element.querySelector(".prev");
-    let next = element.querySelector(".next");
-    let items = await waitForElements(carouselElement, itemSelector);
-
-    // add attributes to first and last items
-    items[0].dataset.first = true;
-    items[items.length - 1].dataset.last = true;
-
-    const carouselClone = carouselElement.cloneNode(true);
-    carouselClone.classList.add("carousel-clone");
-    carouselClone.style.setProperty("position", "absolute");
-    carouselClone.style.setProperty("left", "0");
-    carouselClone.style.setProperty("top", "-100px");
-    carouselClone.style.setProperty("width", "100%");
-    carouselClone.style.setProperty("overflow", "hidden");
-    carouselClone.style.setProperty("flex-wrap", "wrap");
-    carouselClone.style.setProperty("pointer-events", "none");
-    carouselClone.style.setProperty("visibility", "hidden");
-    carouselClone.style.setProperty("height", "100");
-
-    const getItemInformations = () => {
-      carouselElement.before(carouselClone);
-      carouselClone.style.setProperty("width", carouselElement.offsetWidth + "px");
-      const items = carouselClone.querySelectorAll(itemSelector);
-      if (!items.length) return 0;
-      const item2 = items[1];
-      let occupiedSpace = item2.offsetLeft;
-
-      if (occupiedSpace === 0 && item2.offsetTop > 0) occupiedSpace = item2.offsetWidth + 40;
-      const width = item2.offsetWidth;
-      const height = [...items].reduce((acc, item) => (acc < item.offsetHeight ? item.offsetHeight : acc), 0);
-      carouselClone.remove();
-
-      return {
-        occupiedSpace,
-        width,
-        height,
-      };
-    };
-
-    // element.classList.add("carousel-start-init");
-
-    // add the clone the last item to the first place and for the first item to the last place
-    // generic function to clone N last items to the first place and N first items to the last place
-
-    const cloneItems = (items, clones) => {
-      for (let i = 0; i <= clones; i++) {
-        carouselElement.appendChild(items[i].cloneNode(true));
-      }
-    };
-    cloneItems(items, itemsBefore);
-
-    const cloneItemsReverse = (items, clones) => {
-      for (let i = 0; i <= clones; i++) {
-        carouselElement.prepend(items[items.length - 1 - i].cloneNode(true));
-      }
-    };
-
-    cloneItemsReverse(items, itemsAfter);
-
-    const moveNext = function () {
-      if (
-        !infiniteLoop &&
-        element.querySelectorAll(itemSelector)[element.querySelectorAll(itemSelector).length - 1].dataset.last
-      )
-        return;
-      let items = element.querySelectorAll(itemSelector);
-      carouselElement.appendChild(items[0]);
-    };
-    next.addEventListener("click", moveNext);
-
-    const movePrev = function () {
-      let items = element.querySelectorAll(itemSelector);
-      const item = items[itemsBefore];
-      if (!infiniteLoop && item.dataset.first) return;
-      carouselElement.prepend(items[items.length - 1]);
-    };
-    prev.addEventListener("click", movePrev);
-
-    // mobile
-    let startX, moveX;
-
-    carouselElement.addEventListener("touchstart", (e) => {
-      startX = e.touches[0].clientX;
+    const itemsContainer = element.querySelector(itemsContainerSelector);
+    const items = element.querySelectorAll(itemSelector);
+
+    const firstItem = items[0].cloneNode(true);
+    firstItem.innerHTML = "";
+    firstItem.classList.add("carousel-item--clone");
+    itemsContainer.prepend(firstItem);
+    const lastItem = firstItem.cloneNode(true);
+    itemsContainer.append(lastItem);
+
+    element.querySelector(".carousel-button.prev").addEventListener("click", () => {
+      itemsContainer.scrollBy({
+        left: -firstItem.clientWidth,
+        behavior: "smooth",
+      });
     });
 
-    carouselElement.addEventListener("touchend", (e) => {
-      moveX = e.changedTouches[0].clientX - startX;
-      if (Math.abs(moveX) > 50) {
-        if (moveX > 0) {
-          movePrev();
-        } else {
-          moveNext();
-        }
-      }
+    element.querySelector(".carousel-button.next").addEventListener("click", () => {
+      itemsContainer.scrollBy({
+        left: firstItem.clientWidth,
+        behavior: "smooth",
+      });
     });
+  };
 
-    /** The rules are generated from the element width and container width
-     *  The rules are :
-     *  - nth-child(1) is opacity:0 and index -2
-     *  - nth-child(2) is opacity:0 and index -1
-     *  - nth-child(3) is opacity:1 and index 0
-     *  - nth-child(4) is opacity:1 and index 1
-     *  - ...
-     *  - nth-child(maxItems) is opacity:1 and index maxItems - 1
-     *
-     */
-    let styleTag;
-    let lastWindowWidth = -1;
-    const generateStyles = (element) => {
-      const windowWidth = window.innerWidth;
-      if (windowWidth === lastWindowWidth) return;
-      lastWindowWidth = windowWidth;
-      const rules = [];
-
-      // add carousel styles generated from the element width
-      if (!styleTag) {
-        styleTag = document.createElement("style");
-        document.head.appendChild(styleTag);
-      }
-
-      const { occupiedSpace, width: itemWidth, height } = getItemInformations();
-
-      if (occupiedSpace < 100) {
-        console.error("Error in the carousel, no item width detected");
-        return;
-      }
+  [...document.querySelectorAll(selector)].forEach((elm) => {
+    carousel(elm);
+  });
 
-      rules.push(`
-        .carousel-container-${uniqueid} {
-          --item-width: ${itemWidth}px;
-          --item-position: ${occupiedSpace}px;
-          --height: ${height}px;
-        }
-      `);
+  function updateCarouselTabIndexes() {
+    const items = document.querySelectorAll(".carousel-item");
 
-      const carouselWidth = carouselElement.offsetWidth;
-      const maxItems = Math.floor(carouselWidth / occupiedSpace) + 1 + itemsBefore + itemsAfter;
+    items.forEach((item) => {
+      const rect = item.getBoundingClientRect();
+      const isVisible = rect.left >= 0 && rect.right <= window.innerWidth;
 
-      let opacity = 0;
-      for (let i = 1; i <= maxItems; i++) {
-        if ((i >= itemsBefore && i < maxItems - itemsAfter) || i - itemsBefore === 0) {
-          opacity = 1;
-        } else if (i === maxItems - itemsAfter || i === itemsBefore - 1) {
-          opacity = 0.2;
-        } else {
-          opacity = 0;
-        }
+      item.classList.toggle("carousel-item--hidden", !isVisible);
+      const links = item.querySelectorAll("a");
 
-        const index = i - itemsBefore;
-        rules.push(`
-          .carousel-item:nth-child(${i}) {
-            --index: ${index};
-            opacity: ${opacity}; 
-            display: flex;
+      links.forEach((link) => {
+        if (link.getAttribute("aria-hidden") !== "true") {
+          if (isVisible) {
+            link.removeAttribute("tabindex");
+          } else {
+            link.setAttribute("tabindex", "-1");
           }
-        `);
-      }
-
-      styleTag.innerHTML = rules.join("\n");
-    };
-
-    window.addEventListener(
-      "resize",
-      debounce(() => {
-        generateStyles(element);
-      }, 50),
-    );
-    generateStyles(element);
-
-    carouselElement.classList.add("carousel-initialized");
-  };
-
-  /**
-   * Waits for elements matching the selector to be present in the DOM within the given element.
-   * @param {HTMLElement} element - The parent element to observe for the selector.
-   * @param {string} selector - The CSS selector to match the elements.
-   * @returns {Promise<NodeListOf<Element>>} A promise that resolves with the matched elements.
-   */
-  const waitForElements = (element, selector) => {
-    return new Promise((resolve) => {
-      const items = element.querySelectorAll(selector);
-      if (items.length) {
-        resolve(items);
-        return;
-      }
-
-      const observer = new MutationObserver((mutations) => {
-        const items = element.querySelectorAll(selector);
-        if (items.length) {
-          observer.disconnect();
-          resolve(items);
         }
       });
-
-      observer.observe(element, {
-        childList: true,
-        subtree: true,
-      });
     });
-  };
+  }
 
-  [...document.querySelectorAll(selector)].forEach((elm) => {
-    carousel(elm);
-  });
+  document.querySelector(".carousel").addEventListener("scroll", updateCarouselTabIndexes);
+  updateCarouselTabIndexes();
 })();
diff --git a/themes/xen-project/assets/js/carousel.js.bak b/themes/xen-project/assets/js/carousel.js.bak
new file mode 100644 (file)
index 0000000..d6c59ea
--- /dev/null
@@ -0,0 +1,227 @@
+(() => {
+  const /* The `selector` constant is storing a CSS selector string that is used to select elements in
+  the DOM. In this code snippet, the `selector` constant is set to `".carousel-container"`,
+  which means it is targeting elements with the class name "carousel-container". This selector
+  is then used to find and initialize carousel functionality on those elements in the
+  document. */
+    selector = ".carousel-container";
+  const itemSelector = ".carousel-item";
+  const itemsBefore = 2;
+  const itemsAfter = 1;
+
+  const { debounce } = window.XenSiteUtils;
+
+  const carousel = async (element) => {
+    const uniqueid = Math.random().toString(36).substring(2, 15);
+    element.classList.add("carousel-container-" + uniqueid);
+    const infiniteLoop = true;
+    const itemsBefore = 2;
+    const itemsAfter = 2;
+
+    const carouselElement = element.querySelector(".carousel");
+    let prev = element.querySelector(".prev");
+    let next = element.querySelector(".next");
+    let items = await waitForElements(carouselElement, itemSelector);
+
+    // add attributes to first and last items
+    items[0].dataset.first = true;
+    items[items.length - 1].dataset.last = true;
+
+    const carouselClone = carouselElement.cloneNode(true);
+    carouselClone.classList.add("carousel-clone");
+    carouselClone.style.setProperty("position", "absolute");
+    carouselClone.style.setProperty("left", "0");
+    carouselClone.style.setProperty("top", "-100px");
+    carouselClone.style.setProperty("width", "100%");
+    carouselClone.style.setProperty("overflow", "hidden");
+    carouselClone.style.setProperty("flex-wrap", "wrap");
+    carouselClone.style.setProperty("pointer-events", "none");
+    carouselClone.style.setProperty("visibility", "hidden");
+    carouselClone.style.setProperty("height", "100");
+
+    const getItemInformations = () => {
+      carouselElement.before(carouselClone);
+      carouselClone.style.setProperty("width", carouselElement.offsetWidth + "px");
+      const items = carouselClone.querySelectorAll(itemSelector);
+      if (!items.length) return 0;
+      const item2 = items[1];
+      let occupiedSpace = item2.offsetLeft;
+
+      if (occupiedSpace === 0 && item2.offsetTop > 0) occupiedSpace = item2.offsetWidth + 40;
+      const width = item2.offsetWidth;
+      const height = [...items].reduce((acc, item) => (acc < item.offsetHeight ? item.offsetHeight : acc), 0);
+      carouselClone.remove();
+
+      return {
+        occupiedSpace,
+        width,
+        height,
+      };
+    };
+
+    // element.classList.add("carousel-start-init");
+
+    // add the clone the last item to the first place and for the first item to the last place
+    // generic function to clone N last items to the first place and N first items to the last place
+
+    const cloneItems = (items, clones) => {
+      for (let i = 0; i <= clones; i++) {
+        carouselElement.appendChild(items[i].cloneNode(true));
+      }
+    };
+    cloneItems(items, itemsBefore);
+
+    const cloneItemsReverse = (items, clones) => {
+      for (let i = 0; i <= clones; i++) {
+        carouselElement.prepend(items[items.length - 1 - i].cloneNode(true));
+      }
+    };
+
+    cloneItemsReverse(items, itemsAfter);
+
+    const moveNext = function () {
+      if (
+        !infiniteLoop &&
+        element.querySelectorAll(itemSelector)[element.querySelectorAll(itemSelector).length - 1].dataset.last
+      )
+        return;
+      let items = element.querySelectorAll(itemSelector);
+      carouselElement.appendChild(items[0]);
+    };
+    next.addEventListener("click", moveNext);
+
+    const movePrev = function () {
+      let items = element.querySelectorAll(itemSelector);
+      const item = items[itemsBefore];
+      if (!infiniteLoop && item.dataset.first) return;
+      carouselElement.prepend(items[items.length - 1]);
+    };
+    prev.addEventListener("click", movePrev);
+
+    // mobile
+    let startX, moveX;
+
+    carouselElement.addEventListener("touchstart", (e) => {
+      startX = e.touches[0].clientX;
+    });
+
+    carouselElement.addEventListener("touchend", (e) => {
+      moveX = e.changedTouches[0].clientX - startX;
+      if (Math.abs(moveX) > 50) {
+        if (moveX > 0) {
+          movePrev();
+        } else {
+          moveNext();
+        }
+      }
+    });
+
+    /** The rules are generated from the element width and container width
+     *  The rules are :
+     *  - nth-child(1) is opacity:0 and index -2
+     *  - nth-child(2) is opacity:0 and index -1
+     *  - nth-child(3) is opacity:1 and index 0
+     *  - nth-child(4) is opacity:1 and index 1
+     *  - ...
+     *  - nth-child(maxItems) is opacity:1 and index maxItems - 1
+     *
+     */
+    let styleTag;
+    let lastWindowWidth = -1;
+    const generateStyles = (element) => {
+      const windowWidth = window.innerWidth;
+      if (windowWidth === lastWindowWidth) return;
+      lastWindowWidth = windowWidth;
+      const rules = [];
+
+      // add carousel styles generated from the element width
+      if (!styleTag) {
+        styleTag = document.createElement("style");
+        document.head.appendChild(styleTag);
+      }
+
+      const { occupiedSpace, width: itemWidth, height } = getItemInformations();
+
+      if (occupiedSpace < 100) {
+        console.error("Error in the carousel, no item width detected");
+        return;
+      }
+
+      rules.push(`
+        .carousel-container-${uniqueid} {
+          --item-width: ${itemWidth}px;
+          --item-position: ${occupiedSpace}px;
+          --height: ${height}px;
+        }
+      `);
+
+      const carouselWidth = carouselElement.offsetWidth;
+      const maxItems = Math.floor(carouselWidth / occupiedSpace) + 1 + itemsBefore + itemsAfter;
+
+      let opacity = 0;
+      for (let i = 1; i <= maxItems; i++) {
+        if ((i >= itemsBefore && i < maxItems - itemsAfter) || i - itemsBefore === 0) {
+          opacity = 1;
+        } else if (i === maxItems - itemsAfter || i === itemsBefore - 1) {
+          opacity = 0.2;
+        } else {
+          opacity = 0;
+        }
+
+        const index = i - itemsBefore;
+        rules.push(`
+          .carousel-item:nth-child(${i}) {
+            --index: ${index};
+            opacity: ${opacity}; 
+            display: flex;
+          }
+        `);
+      }
+
+      styleTag.innerHTML = rules.join("\n");
+    };
+
+    window.addEventListener(
+      "resize",
+      debounce(() => {
+        generateStyles(element);
+      }, 50),
+    );
+    generateStyles(element);
+
+    carouselElement.classList.add("carousel-initialized");
+  };
+
+  /**
+   * Waits for elements matching the selector to be present in the DOM within the given element.
+   * @param {HTMLElement} element - The parent element to observe for the selector.
+   * @param {string} selector - The CSS selector to match the elements.
+   * @returns {Promise<NodeListOf<Element>>} A promise that resolves with the matched elements.
+   */
+  const waitForElements = (element, selector) => {
+    return new Promise((resolve) => {
+      const items = element.querySelectorAll(selector);
+      if (items.length) {
+        resolve(items);
+        return;
+      }
+
+      const observer = new MutationObserver((mutations) => {
+        const items = element.querySelectorAll(selector);
+        if (items.length) {
+          observer.disconnect();
+          resolve(items);
+        }
+      });
+
+      observer.observe(element, {
+        childList: true,
+        subtree: true,
+      });
+    });
+  };
+
+  [...document.querySelectorAll(selector)].forEach((elm) => {
+    carousel(elm);
+  });
+})();
index 62e59e8ba66623d90aed9d6278a46ce5f4e1cf35..2dd00a851c26d94abfcaefc92bed0af01e70627c 100644 (file)
     };
   };
 
+  /**
+   * Waits for elements matching the selector to be present in the DOM within the given element.
+   * @param {HTMLElement} element - The parent element to observe for the selector.
+   * @param {string} selector - The CSS selector to match the elements.
+   * @returns {Promise<NodeListOf<Element>>} A promise that resolves with the matched elements.
+   */
+  const waitForElements = (element, selector) => {
+    return new Promise((resolve) => {
+      const items = element.querySelectorAll(selector);
+      if (items.length) {
+        resolve(items);
+        return;
+      }
+
+      const observer = new MutationObserver((mutations) => {
+        const items = element.querySelectorAll(selector);
+        if (items.length) {
+          observer.disconnect();
+          resolve(items);
+        }
+      });
+
+      observer.observe(element, {
+        childList: true,
+        subtree: true,
+      });
+    });
+  };
+
   window.XenSiteUtils = {
     formatDate,
     debounce,
+    waitForElements,
   };
 })();
index dd88ce48c8bd28cd5dedb4fe853731f8e748cda6..6ef7da6fb84ad998ec90e2af9571eaf3a47c39be 100644 (file)
@@ -1,7 +1,9 @@
 <div class="carousel-container {{ with .class }}{{ . }}{{ end }}">
   <div class="carousel-content">
-    <div class="carousel">
-      {{ partial "carousel-items.html" (dict "items" .content) }}
+    <div class="carousel-content-inner">
+      <div class="carousel">
+        {{ partial "carousel-items.html" (dict "items" .content) }}
+      </div>
     </div>
     <div class="carousel-buttons">
       <button class="carousel-button prev">