Skip to main
Table of Contents

Slideshow

Below is an example of slideshows, built using the Siema carousel library , along with some improvements made to allow for user input-influenced timer based auto-scrolling and automatic multiple carousel support and initialization.

Demonstration

HTML Only Items

Use any html you want inside of the .slideshow-item class, and the slideshow will handle the rest!

Example slideshow button 1 of 4 image.

Example slideshow html content 1 of 4

Example slideshow button 2 of 4 (2) image.

Example slideshow html content 2 of 4 (2)

Example slideshow button 3 of 4 (2) image.

Example slideshow html content 3 of 4 (2)

Example slideshow button 4 of 4 (2) image.

Example slideshow html content 4 of 4 (2)

Clickable Button Items

The Synticore version of Siema is also capable of handling user inputs with clickable links, without interfering with swiping! You may click the buttons or swipe them, try for yourself:

The Code

Base Files

All base files can be found at https://github.com/pawelgrzybek/siema . The files used are siema.min.css, font-icon-module-siema.min.css, siema.min.js, and siema.custom.min.js. They are loaded in that order, with the CSS in the head of the webpage and the JavaScript at the end of the webpage.

HTML

<section>
  <h2>Demonstration</h2>

  <h3>HTML Only Items</h3>

  <p>Use any html you want inside of the <code class="language-css">.slideshow-item</code> class, and the slideshow will handle the rest!</p>

  <div class="slideshow slideshow--red">
    <div class="slideshow-interact">
      <div class="slideshow-buttons">
        <a class="slideshow-prev" class="button button--slideshow" href="" onclick="event.preventDefault();"></a>
        <a class="slideshow-next" class="button button--slideshow" href="" onclick="event.preventDefault();"></a>
      </div>

      <div class="slideshow-items slideshow-items--init">
        @@include('_html/component/example/slideshow_html.html', {
          "index": "1 of 4",
          "image_filename": "128x256",
          "color_bg": "#ff000033"
        })

        @@include('_html/component/example/slideshow_html.html', {
          "index": "2 of 4 (2)",
          "image_filename": "256x256",
          "color_bg": "#0ff00033"
        })

        @@include('_html/component/example/slideshow_html.html', {
          "index": "3 of 4 (2)",
          "image_filename": "512x256",
          "color_bg": "#0000ff33"
        })

        @@include('_html/component/example/slideshow_html.html', {
          "index": "4 of 4 (2)",
          "image_filename": "600x200",
          "color_bg": "#00000033"
        })
      </div>
    </div>

    <div class="slideshow-timer">
      <div class="slideshow-progress"></div>
    </div>
  </div>

  <h3>Clickable Button Items</h3>

  <p>The Synticore version of Siema is also capable of handling user inputs with clickable links, without interfering with swiping! You may click the buttons or swipe them, try for yourself:</p>

  <div class="slideshow">
    <div class="slideshow-interact">
      <div class="slideshow-buttons">
        <a class="slideshow-prev" class="button button--slideshow" href="" onclick="event.preventDefault();"></a>
        <a class="slideshow-next" class="button button--slideshow" href="" onclick="event.preventDefault();"></a>
      </div>

      <div class="slideshow-items slideshow-items--init">
        @@include('_html/component/example/slideshow_button.html', {
          "index": "1 of 4",
          "image_filename": "128x256",
          "click_text": "You Clicked Button 1"
        })

        @@include('_html/component/example/slideshow_button.html', {
          "index": "2 of 4",
          "image_filename": "256x256",
          "click_text": "You Clicked Button 2"
        })

        @@include('_html/component/example/slideshow_button.html', {
          "index": "3 of 4",
          "image_filename": "512x256",
          "click_text": "You Clicked Button 3"
        })

        @@include('_html/component/example/slideshow_button.html', {
          "index": "4 of 4",
          "image_filename": "600x200",
          "click_text": "You Clicked Button 4"
        })
      </div>
    </div>

    <div class="slideshow-timer">
      <div class="slideshow-progress"></div>
    </div>
  </div>
</section>

Custom CSS

The below CSS is used to get Siema to work the way it does in Synticore.

.button--slideshow{
  -webkit-user-select:none;
  -moz-user-select:none;
  -ms-user-select:none;
  user-select:none
}
.slideshow .slideshow-interact{
  background:#555;
  border:.125rem solid hsla(0,0%,100%,.25);
  border-radius:.5rem;
  position:relative;
  overflow:hidden;
  max-width:600px;
  margin-left:auto;
  margin-right:auto
}
.slideshow .slideshow-interact:hover{
  background:rgba(85,85,85,.8)
}
.slideshow .slideshow-interact .slideshow-buttons{
  position:absolute;
  top:50%;
  -webkit-transform:translateY(-50%);
  transform:translateY(-50%);
  width:100%;
  pointer-events:none;
  z-index:1;
  -webkit-transition-property:opacity;
  transition-property:opacity;
  -webkit-transition-duration:250ms;
  transition-duration:250ms;
  -webkit-transition-timing-function:ease-out;
  transition-timing-function:ease-out
}
.slideshow .slideshow-interact .slideshow-buttons .slideshow-prev,.slideshow .slideshow-interact .slideshow-buttons .slideshow-next{
  border:.125rem solid #333;
  pointer-events:all;
  font-weight:bold;
  padding:1rem .625rem;
  text-decoration:none;
  background-color:#acf;
  color:#000
}
.slideshow .slideshow-interact .slideshow-buttons .slideshow-prev::before,.slideshow .slideshow-interact .slideshow-buttons .slideshow-next::before{
  font-family:"font-icon-module-siema";
  display:inline-block;
  font-size:.75em
}
.slideshow .slideshow-interact .slideshow-buttons .slideshow-prev{
  float:left
}
.slideshow .slideshow-interact .slideshow-buttons .slideshow-prev::before{
  content:""
}
.slideshow .slideshow-interact .slideshow-buttons .slideshow-next{
  float:right
}
.slideshow .slideshow-interact .slideshow-buttons .slideshow-next::before{
  content:""
}
.slideshow .slideshow-interact .slideshow-items>*{
  display:-webkit-box;
  display:-ms-flexbox;
  display:flex;
  -webkit-box-align:center;
  -ms-flex-align:center;
  align-items:center;
  -webkit-box-pack:center;
  -ms-flex-pack:center;
  justify-content:center
}
.slideshow .slideshow-interact .slideshow-items>*>*{
  text-align:center;
  padding-left:2.75rem;
  padding-right:2.75rem
}
.slideshow .slideshow-interact .slideshow-items.slideshow-items--init .slideshow-item--init{
  display:none
}
.slideshow .slideshow-interact .slideshow-items a{
  margin:0px
}
.slideshow .slideshow-interact .slideshow-items .slideshow-item{
  display:inline-block
}
.slideshow .slideshow-interact .slideshow-items .slideshow-item.button{
  padding:0px
}
.slideshow .slideshow-interact .slideshow-items .slideshow-item.button img{
  display:block;
  margin-left:auto;
  margin-right:auto;
  margin-top:0px;
  margin-bottom:0px
}
.slideshow .slideshow-interact .slideshow-items .slideshow-item.button p{
  margin:0px
}
.slideshow .slideshow-interact .slideshow-items .slideshow-item .slideshow-text{
  text-align:center;
  padding:.5rem 1rem
}
.slideshow.slideshow-items--swipe a{
  cursor:-webkit-grabbing;
  cursor:grabbing;
  opacity:1;
  pointer-events:none
}
.slideshow.slideshow-items--swipe .slideshow-buttons{
  opacity:.25
}
.slideshow.slideshow-items--swipe .slideshow-buttons .slideshow-prev,.slideshow.slideshow-items--swipe .slideshow-buttons .slideshow-next{
  pointer-events:none
}
.slideshow .slideshow-timer{
  position:relative;
  width:100%;
  height:.25rem;
  background-color:#555;
  overflow:hidden;
  border-radius:.125rem;
  margin-top:.25rem;
  max-width:600px;
  margin-left:auto;
  margin-right:auto
}
.slideshow .slideshow-timer .slideshow-progress{
  position:absolute;
  height:100%;
  width:0%;
  background-color:#acf
}
.slideshow .slideshow-timer .slideshow-progress:not(.slideshow-progress--reset){
  -webkit-transition:width .1s linear;
  transition:width .1s linear
}

JavaScript

Here is the JavaScript used to initialize the above carousels with Siema.

class FlexibleTimer {
  constructor(element, callback) {
    this.callback = callback;
    this.remainingTime = 0;
    this.startTime = 0;
    this.timerId = null;
    this.isRunning = false;
    this.totalDuration = 0;
    this.progressBar = typeof element === "string" ? document.querySelector(element) : element;
    this.updateProgressBar();
  }

  start(duration = null) {
    if (duration !== null) {
      this.remainingTime = duration;
      this.totalDuration = duration;
    }

    if (this.remainingTime <= 0) {
      return;
    }

    if (this.isRunning) {
      return;
    }

    this.startTime = Date.now();
    this.isRunning = true;

    this.timerId = setInterval(() => {
      const remaining = this.getRemainingTime();
      if (remaining <= 0) {
        this.end();
      } else {
        this.updateProgressBar();
      }
    }, 100);
  }

  end() {
    if (this.timerId) {
      clearInterval(this.timerId);
    }

    this.isRunning = false;
    this.remainingTime = 0;

    if (this.callback) {
      this.callback();
    }

    this.updateProgressBar();
  }

  pause() {
    if (!this.isRunning) {
      return;
    }

    clearInterval(this.timerId);
    this.isRunning = false;
    this.remainingTime -= Date.now() - this.startTime;
    this.updateProgressBar();
  }

  reset(newDuration) {
    this.pause();
    this.remainingTime = newDuration;
    this.totalDuration = newDuration;
    this.updateProgressBar();
  }

  getRemainingTime() {
    if (this.isRunning) {
      return this.remainingTime - (Date.now() - this.startTime);
    }
    return this.remainingTime;
  }

  stop() {
    clearInterval(this.timerId);
    this.isRunning = false;
    this.remainingTime = 0;
    this.updateProgressBar();
  }

  updateProgressBar() {
    if (this.progressBar) {
      const remaining = this.getRemainingTime();
      let percentage = 0;

      if (remaining > 0) {
        percentage = ((this.totalDuration - remaining) / this.totalDuration) * 100;
      }

      this.progressBar.style.width = `${percentage}%`;
    }
  }
}

const SLIDESHOW_AUTO_DELAY = 5000;
const SLIDESHOW_AUTO_DELAY_INTERACTED = 10000;
const SLIDESHOW_AUTO_DELAY_MIN = 2000;

const SWIPE_THRESHOLD = 1;

class Slideshow {
  constructor(element) {
    this.interacted = false;

    this.onSlideshowSlideChange = this.onSlideshowSlideChange.bind(this);
    this.onTimerComplete = this.onTimerComplete.bind(this);
    this.timerStart = this.timerStart.bind(this);
    this.onSlideshowInit = this.onSlideshowInit.bind(this);

    this.elementSlideshowWrap = element;
    this.elementSlideshowInteract = element.querySelector(".slideshow-interact");
    this.elementSlideshow = element.querySelector(".slideshow-items");
    this.elementSlideshowPrev = element.querySelector(".slideshow-prev");
    this.elementSlideshowNext = element.querySelector(".slideshow-next");

    this.elementSlideshowButtons = [this.elementSlideshowPrev, this.elementSlideshowNext];

    this.isInteraction = false;
    this.interactionXInit = 0;
    this.isInteractionSwipe = false;

    this.timer = new FlexibleTimer(element.querySelector(".slideshow-progress"), this.onTimerComplete);

    this.slideshowSiema = new Siema({
      selector: this.elementSlideshow,
      duration: 500,
      easing: "ease-out",
      perPage: 1,
      startIndex: 0,
      draggable: true,
      multipleDrag: true,
      threshold: 50,
      loop: true,
      rtl: false,
      onInit: this.onSlideshowInit,
      onChange: this.onSlideshowSlideChange.bind(this),
    });

    this.isEventOnElement = this.isEventOnElement.bind(this);

    this.slideshowTimerStart = this.slideshowTimerStart.bind(this);
    this.slideshowTimerPause = this.slideshowTimerPause.bind(this);

    this.handleInteractStart = this.handleInteractStart.bind(this);
    this.handleInteractMove = this.handleInteractMove.bind(this);
    this.handleInteractEnd = this.handleInteractEnd.bind(this);
    this.handleInteractEndTouch = this.handleInteractEndTouch.bind(this);

    this.handleButtonAction = this.handleButtonAction.bind(this);

    this.elementSlideshowInteract.addEventListener("mouseover", (event) => {
      if (this.isTouchDevice() || !this.isEventOnElement(this.elementSlideshowInteract, event)) {
        return;
      }

      this.slideshowTimerPause();
    });

    this.elementSlideshowInteract.addEventListener("mouseout", (event) => {
      if (!this.isEventOnElement(this.elementSlideshowInteract, event)) {
        return;
      }

      this.handleInteractEnd(event);
      this.slideshowTimerStart();
    });

    this.elementSlideshow.addEventListener("touchstart", this.handleInteractStart);
    this.elementSlideshow.addEventListener("touchmove", this.handleInteractMove);
    this.elementSlideshow.addEventListener("touchend", this.handleInteractEndTouch);

    this.elementSlideshow.addEventListener("mousedown", this.handleInteractStart);
    this.elementSlideshow.addEventListener("mousemove", this.handleInteractMove);

    this.elementSlideshow.addEventListener("click", this.handleInteractEnd);

    for (const elem of this.elementSlideshowWrap.querySelectorAll("a")) {
      elem.addEventListener("focusin", () => {
        this.slideshowTimerPause();
      });
      elem.addEventListener("focusout", (event) => {
        const newFocusTarget = event.relatedTarget;
        if (!this.elementSlideshowWrap.contains(newFocusTarget)) {
          this.handleInteractEnd(event);
          this.slideshowTimerStart();
        }
      });
    }

    this.elementSlideshowPrev.addEventListener(
      "click",
      this.handleButtonAction(() => this.slideshowSiema.prev())
    );
    this.elementSlideshowNext.addEventListener(
      "click",
      this.handleButtonAction(() => this.slideshowSiema.next())
    );

    this.elementSlideshowButtons.forEach((elementSlideshowButton) => {
      elementSlideshowButton.addEventListener("touchstart", () => {
        this.slideshowTimerPause();
      });
    });

    this.elementSlideshowInteract.addEventListener("touchend", () => {
      this.slideshowTimerStart();
    });

    document.addEventListener("visibilitychange", () => {
      if (document.visibilityState === "visible") {
        this.slideshowTimerStart(SLIDESHOW_AUTO_DELAY);
      } else {
        this.slideshowTimerPause();
      }
    });
  }

  timerStart(duration = 0) {
    if (!this.timer) {
      return;
    }

    if (duration > 0) {
      this.timer.start(duration);
      return;
    }

    if (this.timer.remainingTime > 0) {
      if (this.timer.remainingTime < SLIDESHOW_AUTO_DELAY_MIN) {
        this.timer.remainingTime = SLIDESHOW_AUTO_DELAY_MIN;
      }
      this.timer.start();
    } else {
      this.timer.start(SLIDESHOW_AUTO_DELAY);
      this.interacted = false;
    }
  }

  timerPause() {
    if (this.timer) {
      this.timer.pause();
    }
  }

  onTimerComplete() {
    this.slideshowSiema.next();
    this.timerStart();
  }

  onSlideshowInit() {
    this.elementSlideshow.classList.remove("slideshow-items--init");
    this.timerStart();
  }

  onSlideshowSlideChange() {}

  onSlideshowInteract() {
    this.interacted = true;

    if (this.timer) {
      this.timer.reset(SLIDESHOW_AUTO_DELAY_INTERACTED);
    }
  }

  slideshowButtonAction(action) {
    this.onSlideshowInteract();

    action();
  }

  slideshowTimerPause() {
    this.timerPause();
  }

  slideshowTimerStart(duration = 0) {
    if (duration > 0) {
      this.timerStart(duration);
    }
    this.timerStart();
  }

  setIsInteractionSwipe(value) {
    this.isInteractionSwipe = value;

    if (this.isInteractionSwipe) {
      this.elementSlideshowWrap.classList.add("slideshow-items--swipe");
    } else {
      this.elementSlideshowWrap.classList.remove("slideshow-items--swipe");
    }
  }

  getEventInteract(event) {
    return event.touches ? event.touches[0] : event;
  }

  handleInteractStart(event) {
    if (event.type === "mousedown" || event.type === "touchstart") {
      this.isInteraction = true;
      this.interactionXInit = this.getEventInteract(event).clientX;
    }

    this.setIsInteractionSwipe(false);

    this.slideshowTimerPause();
  }

  handleInteractMove(event) {
    if (!this.isInteraction || this.isInteractionSwipe) return;

    const deltaX = this.getEventInteract(event).clientX - this.interactionXInit;

    if (Math.abs(deltaX) > SWIPE_THRESHOLD) {
      this.setIsInteractionSwipe(true);
    }
  }

  isInteractLink(event) {
    let target = event.target;

    while (target && target !== this.elementSlideshow) {
      if (target.tagName.toLowerCase() === "a") {
        return true;
      }

      target = target.parentElement;
    }

    return false;
  }

  handleSwipeEnd(event) {
    if (this.isInteractionSwipe) {
      if (this.isInteractLink(event)) {
        event.preventDefault();
        event.stopPropagation();
      }

      this.setIsInteractionSwipe(false);
    }
  }

  handleInteractEnd(event) {
    this.handleSwipeEnd(event);

    if (this.isInteraction) {
      this.onSlideshowInteract();
    }

    this.isInteraction = false;
  }

  isEventOnElement(element, event) {
    return !element.contains(event.relatedTarget);
  }

  isTouchDevice() {
    return "ontouchstart" in window || navigator.maxTouchPoints > 0;
  }

  handleInteractEndTouch(event) {
    if (!this.isEventOnElement(elementSlideshowInteract, event)) {
      return;
    }

    this.handleInteractEnd(event);
    this.slideshowTimerStart();
  }

  handleButtonAction(action) {
    return () => {
      this.slideshowButtonAction(() => action());

      if (!this.isTouchDevice()) {
        return;
      }

      this.slideshowTimerStart();
    };
  }
}

const elementSlideshowWraps = document.querySelectorAll(".slideshow");
let slideshows = [];

for (const elementSlideshowWrap of elementSlideshowWraps) {
  slideshows.push(new Slideshow(elementSlideshowWrap));
}

Sources

This page is comprised of my own additions and either partially or heavily modified elements from the following source(s):