Web Design Guide 2023-12-11 10:29:21 | 2.0.0

Spinner

<div id="" class=" spinner -ar-7x5 ">
    <ol class="spinner__images">
        <li class="spinner__image -active">
            <div class=" media ">
                <img class="media__item" src="/demo/teaser/teaser1.jpg" alt=>
            </div>

        </li>
        <li class="spinner__image ">
            <div class=" media ">
                <img class="media__item" src="/demo/teaser/teaser2.jpg" alt=>
            </div>

        </li>
        <li class="spinner__image ">
            <div class=" media ">
                <img class="media__item" src="/demo/teaser/teaser3.jpg" alt=>
            </div>

        </li>
    </ol>
    <div class="spinner__icon" style="display: nonee;"></div>
</div>
<div id="" class="{{mixes}} spinner -ar-7x5 {{modifiers}}">{{!-- cm-spinner__canvas --}}
  <ol class="spinner__images"> {{!-- cm-spinner__images cm-details__media-box --}}
    {{#each images}}
      <li class="spinner__image {{modifiers}}"> {{!-- cm-spinner__image --}}
        {{render '@atoms-media--image' image}}
      </li>
    {{/each}}
  </ol>
  <div class="spinner__icon" style="display: nonee;"></div>{{!-- cm-spinner__icon --}}
</div>
{
  "images": [
    {
      "modifiers": "-active",
      "image": {
        "src": "/demo/teaser/teaser1.jpg"
      }
    },
    {
      "image": {
        "src": "/demo/teaser/teaser2.jpg"
      }
    },
    {
      "image": {
        "src": "/demo/teaser/teaser3.jpg"
      }
    }
  ]
}
  • Content:
    .spinner {
      position: relative;
      overflow: hidden;
      cursor: ew-resize;
      touch-action: none;
    
      &__images {
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        display: block;
        width: 100%;
        height: 100%;
        margin: 0;
        padding: 0;
        list-style: none;
      }
    
      &__image {
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        display: block;
        width: 100%;
        height: 100%;
        visibility: hidden;
    
        &.-active {
          visibility: visible;
        }
      }
    
      &__icon {
        position: absolute;
        right: 50%;
        bottom: 50%;
        width: 9rem;
        max-width: 75%;
        height: 9rem;
        max-height: 75%;
        transform: translate(50%, 50%);
        background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj4NCiAgPHN0eWxlPg0KICAgIC5zdDAgew0KICAgICAgZmlsbDogcmdiYSgwLCAwLCAwLCAwLjUpOw0KICAgICAgc3Ryb2tlOiAjRkZGRkZGOw0KICAgICAgc3Ryb2tlLXdpZHRoOiAzMDsNCiAgICAgIHN0cm9rZS1taXRlcmxpbWl0OiAxMDsNCiAgICB9DQogICAgLnN0MXsNCiAgICAgIGZpbGw6ICNGRkZGRkY7DQogICAgfQ0KICA8L3N0eWxlPg0KICA8Y2lyY2xlIGNsYXNzPSJzdDAiIGN4PSIyNTYiIGN5PSIyNTYiIHI9IjIzMSIvPg0KICA8cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTI1LjQgMjQ4LjlIMTM4YzYgMCAxMC40LTEuNSAxMy4zLTQuNSAyLjktMyA0LjMtNyA0LjMtMTEuOSAwLTQuOC0xLjQtOC41LTQuMy0xMS4yLTIuOS0yLjctNi44LTQtMTEuOC00LTQuNSAwLTguMyAxLjItMTEuMyAzLjdzLTQuNiA1LjctNC42IDkuN0gxMDBjMC02LjIgMS43LTExLjggNS0xNi43IDMuMy00LjkgOC04LjggMTQtMTEuNSA2LTIuOCAxMi42LTQuMiAxOS45LTQuMiAxMi42IDAgMjIuNCAzIDI5LjUgOSA3LjEgNiAxMC43IDE0LjMgMTAuNyAyNC44IDAgNS40LTEuNyAxMC40LTUgMTVzLTcuNyA4LjEtMTMgMTAuNWM2LjcgMi40IDExLjcgNiAxNSAxMC44IDMuMyA0LjggNC45IDEwLjQgNC45IDE3IDAgMTAuNS0zLjggMTktMTEuNSAyNS40LTcuNyA2LjQtMTcuOSA5LjUtMzAuNSA5LjUtMTEuOSAwLTIxLjUtMy4xLTI5LjEtOS40LTcuNS02LjMtMTEuMy0xNC41LTExLjMtMjQuOGgyMy42YzAgNC41IDEuNyA4LjEgNSAxMC45IDMuMyAyLjggNy41IDQuMiAxMi40IDQuMiA1LjYgMCAxMC0xLjUgMTMuMi00LjQgMy4yLTMgNC44LTYuOSA0LjgtMTEuOCAwLTExLjktNi41LTE3LjgtMTkuNi0xNy44aC0xMi41di0xOC4zek0yNjEuMSAxOTguOHYxOS40aC0yLjNjLTEwLjcuMi0xOS4yIDIuOS0yNS43IDguM3MtMTAuNCAxMi45LTExLjcgMjIuNGM2LjMtNi40IDE0LjMtOS42IDIzLjktOS42IDEwLjMgMCAxOC41IDMuNyAyNC42IDExLjEgNi4xIDcuNCA5LjEgMTcuMSA5LjEgMjkuMiAwIDcuNy0xLjcgMTQuNy01IDIxLTMuMyA2LjMtOC4xIDExLjEtMTQuMiAxNC42cy0xMyA1LjItMjAuOCA1LjJjLTEyLjUgMC0yMi42LTQuMy0zMC4zLTEzLTcuNy04LjctMTEuNS0yMC4zLTExLjUtMzQuOFYyNjRjMC0xMi45IDIuNC0yNC4zIDcuMy0zNC4xIDQuOS05LjkgMTEuOS0xNy41IDIxLTIyLjkgOS4xLTUuNCAxOS43LTguMSAzMS43LTguMmgzLjl6bS0yMyA1OS41Yy0zLjggMC03LjMgMS0xMC40IDMtMy4xIDItNS40IDQuNi02LjggNy45djcuMmMwIDcuOSAxLjUgMTQgNC42IDE4LjQgMy4xIDQuNCA3LjQgNi42IDEzIDYuNiA1LjEgMCA5LjEtMiAxMi4zLTYgMy4xLTQgNC43LTkuMiA0LjctMTUuNSAwLTYuNS0xLjYtMTEuNy00LjctMTUuNi0zLjEtNC03LjMtNi0xMi43LTZ6TTM3Mi45IDI2OS42YzAgMTYuNC0zLjQgMjktMTAuMiAzNy43LTYuOCA4LjctMTYuNyAxMy0yOS44IDEzLTEyLjkgMC0yMi44LTQuMy0yOS43LTEyLjgtNi44LTguNS0xMC40LTIwLjgtMTAuNS0zNi43VjI0OWMwLTE2LjYgMy40LTI5LjIgMTAuMy0zNy44IDYuOS04LjYgMTYuOC0xMi45IDI5LjctMTIuOXMyMi44IDQuMyAyOS43IDEyLjhjNi44IDguNSAxMC40IDIwLjcgMTAuNSAzNi43djIxLjh6bS0yMy42LTI0YzAtOS44LTEuMy0xNy00LTIxLjVzLTYuOS02LjctMTIuNi02LjdjLTUuNSAwLTkuNiAyLjEtMTIuMyA2LjQtMi42IDQuMy00IDExLTQuMiAyMHYyOC45YzAgOS43IDEuMyAxNi45IDQgMjEuNiAyLjYgNC43IDYuOSA3LjEgMTIuNyA3LjEgNS44IDAgOS45LTIuMyAxMi41LTYuOCAyLjYtNC41IDMuOS0xMS40IDQtMjAuN3YtMjguM3pNMzkxIDIyMC44YzAtNi4yIDIuMi0xMS41IDYuNi0xNS45IDQuNC00LjQgOS42LTYuNiAxNS42LTYuNiA1LjkgMCAxMSAyLjIgMTUuNCA2LjYgNC4zIDQuNCA2LjUgOS43IDYuNSAxNS45IDAgNi4zLTIuMiAxMS41LTYuNSAxNS44LTQuMyA0LjMtOS40IDYuNC0xNS40IDYuNC01LjkgMC0xMS0yLjEtMTUuNS02LjQtNC41LTQuMi02LjctOS41LTYuNy0xNS44em0yMi4yIDEwLjJjMi45IDAgNS4zLTEgNy4xLTIuOSAxLjktMS45IDIuOC00LjMgMi44LTcuMiAwLTIuOC0uOS01LjMtMi44LTcuMy0xLjktMi4xLTQuMy0zLjEtNy4xLTMuMS0yLjkgMC01LjMgMS03LjMgMy4xcy0yLjkgNC41LTIuOSA3LjMgMSA1LjIgMyA3LjIgNC40IDIuOSA3LjIgMi45eiIvPg0KPC9zdmc+');
      }
    }
    
  • URL: /components/raw/molecules-spinner/_spinner.scss
  • Filesystem Path: src/components/02-ui-components/02-molecules/22-spinner/_spinner.scss
  • Size: 3.8 KB

Requeres jQuery plugin: https://github.com/heartcode/360-Image-Slider

Example code snippet from coremedia9:

import $ from "jquery";
import * as logger from '@coremedia/js-logger';

function threeSixtySpinner(spinner) {
  logger.log("[360]", spinner);

  const SPINNER_IMAGE_SELECTOR = '.spinner__image';
  const SPINNER_ACTIVE_IMAGE_CLASS = '-active';

  const $document = $(document);
  const $container = $(spinner);
  const images = $container.find(SPINNER_IMAGE_SELECTOR);

  const totalFrames = images.length;

  // Defaults parameters:
  let ready = false;
  let dragging = false;
  let moved = false;
  let pointerStartPosX = 0;
  let pointerEndPosX = 0;
  let pointerDistance = 0;
  // The starting time of the pointer tracking period
  let monitorStartTime = 0;
  // The pointer tracking time duration
  const monitorInt = 40;
  // A setInterval instance used to call the rendering function
  let ticker = 0;
  // Sets the speed of the image sliding animation
  const speedMultiplier = 0.5;
  // The current frame value of the image slider animation
  let currentFrame = 0;
  // Stores all the loaded image objects
  const frames = [];
  // The value of the end frame which the currentFrame will be tweened to during the sliding animation
  let endFrame = 0;
  logger.log("[360]",totalFrames,images);
  if (totalFrames > 1) {
    logger.log("[360] Initialize 360° Spinner with " + totalFrames + " frames");

    // Inititialize frames (array of image objects)
    images.each(function () {
      frames.push($(this));
    });

    // Show first frame
    frames[0].addClass(SPINNER_ACTIVE_IMAGE_CLASS).fadeIn();

    //Start
    render();

    /* --- Events --- */

    /**
     * Adds the jQuery "mousedown" and "touchstart" event to the image slider wrapper.
     */
    $container.on("mousedown touchstart", function (event) {
      // leftclick
      if (event.type == "mousedown" && event.which == 1) {
        ready = true;
        event.preventDefault();
        event.stopPropagation(); //w3c
        event.cancelBubble = true; //ie
        // touch
      } else if (event.type == "touchstart") {
        ready = true;
        // stop swipe on touch devices
        event.stopPropagation();
      }
      // start
      if (ready) {
        // Stores the pointer x position as the starting position
        pointerStartPosX = getPointerEvent(event).pageX;
        logger.log("360° Spinner: start dragging by " + event.type);
        // Remove Icon
        hideIcon();
      }
    });

    /**
     * Add the jQuery "mousemove" and "touchmove" event handler, if started dragging inside the $container.
     */
    $document.on("mousemove touchmove", function (event) {
      if (ready) {
        dragging = true;

        event.preventDefault();
        event.stopPropagation();
        // Starts tracking the pointer X position changes
        trackPointer(event);
      }
    });

    /**
     * Adds the jQuery "mouseup" event to the document for stopping, if started dragging inside the $container.
     */
    $document.on("mouseup touchend", function (event) {
      ready = false;
      if (dragging) {
        dragging = false;
        event.preventDefault();
        event.stopImmediatePropagation();
        // remove close click stop  to enable outside click again
        /*        window.setTimeout(function () {
                  $container.closest(".mfp-container").attr("data-stopclosing", "false");
                }, 100);*/

        logger.log("360° Spinner: stop dragging");
      }
    });

    /**
     * Adds the jQuery "keydown" event to the document. You can move the spinner by pressing left or right on the keyboard.
     */
    $document.on("keydown", function (event) {
      // only if spinner is visible
      if ($container.css("visibility") !== 'hidden') {
        const key = event.keyCode || event.which;
        switch (key) {
          // left key, go one frame to the left
          case 37:
            endFrame--;
            moved = true;
            break;
          // right key, go one frame to the right
          case 39:
            endFrame++;
            moved = true;
            break;
        }
        if (moved) {
          logger.log("360° Spinner: Moved with keyboard");
          hideIcon();
          render();
          moved = false;
        }
      }
    });

  } else {
    logger.log("[360] Error: Found 360° Spinner without frames, can't initialize it.");
  }

  /* --- internal functions --- */

  /**
   * Renders the image slider frame animations.
   */

  function render() {
    // The rendering function only runs if the "currentFrame" value hasn't reached the "endFrame" one
    if (currentFrame !== endFrame) {
      // Calculates the 10% of the distance between the "currentFrame" and the "endFrame".
      // By adding only 10% we get a nice smooth and eased animation.
      // If the distance is a positive number, we have to ceil the value, if its a negative number, we have to floor it to make sure
      // that the "currentFrame" value surely reaches the "endFrame" value and the rendering doesn't end up in an infinite loop.
      const frameEasing = endFrame < currentFrame ? Math.floor((endFrame - currentFrame) * 0.1) : Math.ceil((endFrame - currentFrame) * 0.1);
      // Sets the current image to be hidden
      hidePreviousFrame();
      // Increments / decrements the "currentFrame" value by the 10% of the frame distance
      currentFrame += frameEasing;
      // Sets the current image to be visible
      showCurrentFrame();
    } else {
      // If the rendering can stop, we stop and clear the ticker
      window.clearInterval(ticker);
      ticker = 0;
    }
  }

  /**
   * Creates a new setInterval and stores it in the "ticker"
   * By default I set the FPS value to 60 which gives a nice and smooth rendering in newer browsers
   * and relatively fast machines, but obviously it could be too high for an older architecture.
   */
  function refresh() {
    // If the ticker is not running already...
    if (ticker === 0) {
      // Let's create a new one!
      ticker = self.setInterval(render, Math.round(1000 / 60));
    }
  }

  /**
   * Hides the previous frame
   * It calls the "getNormalizedCurrentFrame" method to translate the "currentFrame" value to the "totalFrames" range
   */
  function hidePreviousFrame() {
    frames[getNormalizedCurrentFrame()].removeClass(SPINNER_ACTIVE_IMAGE_CLASS); //TODO Refactor to constant
  }

  /**
   * Displays the current frame
   * It calls the "getNormalizedCurrentFrame" method to translate the "currentFrame" value to the "totalFrames" range
   */
  function showCurrentFrame() {
    frames[getNormalizedCurrentFrame()].addClass(SPINNER_ACTIVE_IMAGE_CLASS);
  }

  /**
   * Returns the "currentFrame" value translated to a value inside the range of 0 and "totalFrames"
   */
  function getNormalizedCurrentFrame() {
    let c = Math.ceil(currentFrame % totalFrames);
    if (c < 0) {
      c += (totalFrames - 1);
    }
    return c;
  }

  /**
   * Returns a simple event regarding the original event is a mouse event or a touch event.
   */
  function getPointerEvent(event) {
    return event.originalEvent.targetTouches ? event.originalEvent.targetTouches[0] : event;
  }

  /**
   * Tracks the pointer X position changes and calculates the "endFrame" for the image slider frame animation.
   * This function only runs if the application is ready and the user really is dragging the pointer; this way we
   * can avoid unnecessary calculations and CPU usage.
   */
  function trackPointer(event) {
    const userDragging = ready && dragging;
    if (userDragging) {
      // Stores the last x position of the pointer
      pointerEndPosX = getPointerEvent(event).pageX;
      // Checks if there is enough time past between this and the last time period of tracking
      if (monitorStartTime < new Date().getTime() - monitorInt) {
        // Calculates the distance between the pointer starting and ending position during the last tracking time period
        pointerDistance = pointerEndPosX - pointerStartPosX;
        // Calculates the endFrame using the distance between the pointer X starting and ending positions and the "speedMultiplier" values
        if (pointerDistance > 0) {
          endFrame = currentFrame + Math.ceil((totalFrames - 1) * speedMultiplier * (pointerDistance / $document.width()));
        } else {
          endFrame = currentFrame + Math.floor((totalFrames - 1) * speedMultiplier * (pointerDistance / $document.width()));
        }
        // Updates the image slider frame animation
        refresh();
        // restarts counting the pointer tracking period
        monitorStartTime = new Date().getTime();
        // Stores the the pointer X position as the starting position (because we started a new tracking period)
        pointerStartPosX = getPointerEvent(event).pageX;
      }
    }
  }

  /**
   * Fade out overlay icon, if displayed
   */
  function hideIcon() {
    const $icon = $container.find('.spinner__icon');
    if ($icon.length) {
      $icon.fadeOut();
    }
  }

}

export default function (domElementOrJQueryResult) {
  if (domElementOrJQueryResult instanceof $) {
    $.each(domElementOrJQueryResult, function (index, item) {
      threeSixtySpinner(item);
    });
  } else {
    threeSixtySpinner(domElementOrJQueryResult);
  }
}