Swipe Project Details

Description

  • able to swipe left/right to navigation to another items, similar when clicking pagination title/arrow, support trackpad
  • view demo – password: abc
  • buy me a coffee

#1. Install Code

#1.1. Click Gear icon on Portfolio Page

07.26c21v1 Portfolio Naughty Carousel

#1.2. Click Advanced > Paste this code

  • If you use Personal/Basic Plan and your plan doesn’t support Injection, see #3.1
<!-- 07.26c24v5 Swipe Project Details -->
<script>
window.SwipeProjectDetailsOptions = {
  enabled: true,
  touchSwipe: true,
  penSwipe: true,
  mouseDrag: true,
  trackpadSwipe: true,
  minimumDistance: 60,
  trackpadMinimumDistance: 80,
  maximumDuration: 1000,
  axisLockDistance: 10,
  directionRatio: 1.25,
  edgeExclusion: 24,
  trackpadIdleDuration: 180,
  navigationCooldown: 1200,
  trackpadInvertDirection: false,
  swipeLeftTarget: 'previous',
  swipeRightTarget: 'next'
};
</script>
<script>
(() => {
  'use strict';

  const defaults = {
    enabled: true,
    touchSwipe: true,
    penSwipe: true,
    mouseDrag: true,
    trackpadSwipe: true,
    minimumDistance: 60,
    trackpadMinimumDistance: 80,
    maximumDuration: 1000,
    axisLockDistance: 10,
    directionRatio: 1.25,
    edgeExclusion: 24,
    trackpadIdleDuration: 180,
    navigationCooldown: 1200,
    trackpadInvertDirection: false,
    swipeLeftTarget: 'previous',
    swipeRightTarget: 'next'
  };

  const options = {
    ...defaults,
    ...(window.SwipeProjectDetailsOptions || {})
  };

  const globalKey = '__swipeProjectDetails__';
  const styleId = 'swipe-project-details-style';
  const activeClass = 'swipe-project-details-active';
  const draggingClass = 'swipe-project-details-dragging';
  const ignoreSelector = [
    '#itemPagination',
    'button',
    'input',
    'textarea',
    'select',
    'option',
    'label',
    'summary',
    '[contenteditable]:not([contenteditable="false"])',
    '[role="button"]',
    '[role="slider"]',
    '[role="application"]',
    'video',
    'audio',
    'iframe',
    'embed',
    'object',
    'canvas',
    '[data-swipe-navigation-ignore]',
    '.sqs-gallery-design-carousel',
    '.sqs-gallery-design-slideshow',
    '.user-items-list-carousel',
    '[data-controller*="Carousel"]',
    '.swiper',
    '.flickity-enabled',
    '.splide',
    '.glide'
  ].join(',');

  const previousController = window[globalKey];
  let lastNavigationTime =
    Number(previousController?.lastNavigationTime) || 0;

  previousController?.destroy?.();
  document.getElementById(styleId)?.remove();

  const style = document.createElement('style');
  style.id = styleId;
  style.textContent = `
    html.${activeClass},
    html.${activeClass} body {
      overscroll-behavior-x: none;
    }

    html.${draggingClass},
    html.${draggingClass} * {
      -webkit-user-select: none !important;
      user-select: none !important;
      cursor: grabbing !important;
    }
  `;
  (document.head || document.documentElement).appendChild(style);

  let touchGesture = null;
  let pointerGesture = null;
  let wheelGesture = null;
  let wheelTimer = 0;
  let lastTouchTime = 0;
  let suppressClickUntil = 0;
  let suppressClickTarget = null;

  function numberOption(name, minimum) {
    const value = Number(options[name]);
    return Number.isFinite(value)
      ? Math.max(minimum, value)
      : defaults[name];
  }

  function editorActive() {
    return Boolean(
      location.pathname === '/config' ||
      location.pathname.startsWith('/config/') ||
      document.body?.classList.contains('sqs-edit-mode-active') ||
      document.documentElement.classList.contains(
        'sqs-edit-mode-active'
      )
    );
  }

  function pageArea() {
    return (
      document.querySelector('main#page') ||
      document.querySelector('main[role="main"]') ||
      document.querySelector('main')
    );
  }

  function pageActive() {
    return Boolean(
      options.enabled !== false &&
      document.body?.classList.contains('view-item') &&
      document.querySelector('#itemPagination') &&
      !editorActive()
    );
  }

  function syncPageClass() {
    document.documentElement.classList.toggle(
      activeClass,
      pageActive()
    );
  }

  function elementFrom(target) {
    return target instanceof Element
      ? target
      : target?.parentElement || null;
  }

  function hasHorizontalScroller(element, area) {
    let current = element;

    while (current && current !== area) {
      const overflow = getComputedStyle(current).overflowX;

      if (
        /^(auto|scroll|overlay)$/.test(overflow) &&
        current.scrollWidth > current.clientWidth + 2
      ) {
        return true;
      }

      current = current.parentElement;
    }

    return false;
  }

  function allowedTarget(target) {
    syncPageClass();

    const area = pageArea();
    const element = elementFrom(target);

    if (!pageActive() || !area || !element) return null;
    if (!area.contains(element)) return null;
    if (element.closest(ignoreSelector)) return null;
    if (hasHorizontalScroller(element, area)) return null;

    return element;
  }

  function createGesture(x, y, id, type, target, checkEdge) {
    const element = allowedTarget(target);
    if (!element) return null;

    if (checkEdge) {
      const edge = Math.min(
        numberOption('edgeExclusion', 0),
        Math.max(0, window.innerWidth / 2 - 1)
      );

      if (x <= edge || x >= window.innerWidth - edge) {
        return null;
      }
    }

    return {
      id,
      type,
      target: element,
      captureTarget: null,
      axis: '',
      startX: x,
      startY: y,
      lastX: x,
      lastY: y,
      startTime: Date.now(),
      dragging: false
    };
  }

  function updateGesture(gesture, x, y) {
    gesture.lastX = x;
    gesture.lastY = y;

    if (gesture.axis) return gesture.axis;

    const horizontal = Math.abs(x - gesture.startX);
    const vertical = Math.abs(y - gesture.startY);
    const lockDistance = numberOption('axisLockDistance', 1);
    const ratio = numberOption('directionRatio', 1);

    if (Math.max(horizontal, vertical) < lockDistance) {
      return '';
    }

    if (horizontal >= vertical * ratio) {
      gesture.axis = 'horizontal';
    } else if (vertical >= horizontal * ratio) {
      gesture.axis = 'vertical';
    }

    return gesture.axis;
  }

  function navigationTarget(gesture, x, y) {
    const horizontal = x - gesture.startX;
    const vertical = y - gesture.startY;
    const horizontalDistance = Math.abs(horizontal);
    const verticalDistance = Math.abs(vertical);
    const elapsed = Date.now() - gesture.startTime;

    if (
      horizontalDistance < numberOption('minimumDistance', 1) ||
      elapsed > numberOption('maximumDuration', 1) ||
      horizontalDistance <
        verticalDistance * numberOption('directionRatio', 1)
    ) {
      return '';
    }

    return horizontal < 0
      ? options.swipeLeftTarget
      : options.swipeRightTarget;
  }

  function paginationLink(target) {
    if (target === 'previous') {
      return document.querySelector(
        '#itemPagination .item-pagination-link--prev'
      );
    }

    if (target === 'next') {
      return document.querySelector(
        '#itemPagination .item-pagination-link--next'
      );
    }

    return null;
  }

  function preventNativeNavigation(event) {
    if (event?.cancelable) event.preventDefault();
  }

  function navigate(target) {
    const now = Date.now();
    const cooldown = numberOption('navigationCooldown', 0);

    if (now - lastNavigationTime < cooldown) return false;

    const link = paginationLink(target);
    if (!link?.href) return false;

    lastNavigationTime = now;
    link.click();
    return true;
  }

  function completeGesture(gesture, x, y, event) {
    const target = navigationTarget(gesture, x, y);
    if (!target) return false;

    preventNativeNavigation(event);
    navigate(target);
    return true;
  }

  function findTouch(list, id) {
    return Array.from(list || []).find(
      (touch) => touch.identifier === id
    );
  }

  function onTouchStart(event) {
    lastTouchTime = Date.now();

    if (
      options.touchSwipe === false ||
      event.touches.length !== 1
    ) {
      touchGesture = null;
      return;
    }

    const touch = event.touches[0];
    touchGesture = createGesture(
      touch.clientX,
      touch.clientY,
      touch.identifier,
      'touch',
      event.target,
      true
    );
  }

  function onTouchMove(event) {
    lastTouchTime = Date.now();
    if (!touchGesture) return;

    if (event.touches.length !== 1) {
      touchGesture = null;
      return;
    }

    const touch = findTouch(
      event.touches,
      touchGesture.id
    );

    if (!touch) {
      touchGesture = null;
      return;
    }

    const axis = updateGesture(
      touchGesture,
      touch.clientX,
      touch.clientY
    );

    if (axis === 'vertical') {
      touchGesture = null;
    } else if (axis === 'horizontal') {
      preventNativeNavigation(event);
    }
  }

  function onTouchEnd(event) {
    lastTouchTime = Date.now();
    if (!touchGesture) return;

    const gesture = touchGesture;
    const touch = findTouch(event.changedTouches, gesture.id);
    touchGesture = null;

    if (touch) {
      completeGesture(
        gesture,
        touch.clientX,
        touch.clientY,
        event
      );
    }
  }

  function onTouchCancel(event) {
    lastTouchTime = Date.now();
    touchGesture = null;
  }

  function pointerAllowed(event) {
    if (event.isPrimary === false || event.button !== 0) {
      return false;
    }

    if (event.pointerType === 'mouse') {
      return Boolean(
        options.mouseDrag &&
        Date.now() - lastTouchTime > 800
      );
    }

    if (event.pointerType === 'pen') {
      return options.penSwipe !== false;
    }

    return false;
  }

  function setPointerCapture(gesture, target) {
    if (!(target instanceof Element)) return;

    try {
      target.setPointerCapture?.(gesture.id);
      gesture.captureTarget = target;
    } catch {
      gesture.captureTarget = null;
    }
  }

  function releasePointerCapture(gesture) {
    const target = gesture?.captureTarget;
    if (!target) return;

    try {
      if (target.hasPointerCapture?.(gesture.id)) {
        target.releasePointerCapture(gesture.id);
      }
    } catch {
      return;
    }
  }

  function beginMousePresentation(gesture) {
    if (gesture.dragging) return;

    gesture.dragging = true;
    document.documentElement.classList.add(draggingClass);
    window.getSelection?.()?.removeAllRanges?.();
  }

  function endPointerGesture() {
    releasePointerCapture(pointerGesture);
    pointerGesture = null;
    document.documentElement.classList.remove(draggingClass);
  }

  function onPointerDown(event) {
    if (!pointerAllowed(event)) return;

    endPointerGesture();
    pointerGesture = createGesture(
      event.clientX,
      event.clientY,
      event.pointerId,
      event.pointerType,
      event.target,
      false
    );

    if (pointerGesture) {
      setPointerCapture(pointerGesture, event.target);
    }
  }

  function onPointerMove(event) {
    if (
      !pointerGesture ||
      pointerGesture.id !== event.pointerId ||
      pointerGesture.type !== event.pointerType
    ) {
      return;
    }

    const axis = updateGesture(
      pointerGesture,
      event.clientX,
      event.clientY
    );

    if (axis === 'vertical') {
      endPointerGesture();
      return;
    }

    if (axis === 'horizontal') {
      beginMousePresentation(pointerGesture);
      preventNativeNavigation(event);
    }
  }

  function onPointerUp(event) {
    if (
      !pointerGesture ||
      pointerGesture.id !== event.pointerId ||
      pointerGesture.type !== event.pointerType
    ) {
      return;
    }

    const gesture = pointerGesture;
    updateGesture(gesture, event.clientX, event.clientY);
    const completed = completeGesture(
      gesture,
      event.clientX,
      event.clientY,
      event
    );

    if (completed) {
      suppressClickUntil = Date.now() + 500;
      suppressClickTarget = gesture.target;
    }

    endPointerGesture();
  }

  function onPointerCancel(event) {
    if (
      !pointerGesture ||
      pointerGesture.id !== event.pointerId ||
      pointerGesture.type !== event.pointerType
    ) {
      return;
    }

    endPointerGesture();
  }

  function onDragStart(event) {
    if (
      pointerGesture?.type === 'mouse' &&
      allowedTarget(event.target)
    ) {
      preventNativeNavigation(event);
    }
  }

  function onClick(event) {
    const target = elementFrom(event.target);
    const source = suppressClickTarget;
    const matchingTarget = Boolean(
      source &&
      target &&
      (
        source === target ||
        source.contains(target) ||
        target.contains(source)
      )
    );

    if (
      event.isTrusted &&
      Date.now() < suppressClickUntil &&
      matchingTarget
    ) {
      suppressClickUntil = 0;
      suppressClickTarget = null;
      event.preventDefault();
      event.stopImmediatePropagation();
    }
  }

  function normalizeWheelDelta(event) {
    const multiplier =
      event.deltaMode === 1
        ? 16
        : event.deltaMode === 2
          ? Math.max(window.innerWidth, 1)
          : 1;

    return {
      x: event.deltaX * multiplier,
      y: event.deltaY * multiplier
    };
  }

  function resetWheelGesture() {
    clearTimeout(wheelTimer);
    wheelTimer = 0;
    wheelGesture = null;
  }

  function scheduleWheelReset() {
    clearTimeout(wheelTimer);
    wheelTimer = window.setTimeout(
      resetWheelGesture,
      numberOption('trackpadIdleDuration', 1)
    );
  }

  function wheelNavigationTarget(horizontal) {
    const leftward = options.trackpadInvertDirection
      ? horizontal < 0
      : horizontal > 0;

    return leftward
      ? options.swipeLeftTarget
      : options.swipeRightTarget;
  }

  function onWheel(event) {
    const now = Date.now();
    const idleDuration = numberOption(
      'trackpadIdleDuration',
      1
    );

    if (
      wheelGesture &&
      now - wheelGesture.lastTime > idleDuration
    ) {
      resetWheelGesture();
    }

    if (
      options.trackpadSwipe === false ||
      event.ctrlKey ||
      event.shiftKey ||
      !allowedTarget(event.target)
    ) {
      resetWheelGesture();
      return;
    }

    const delta = normalizeWheelDelta(event);
    if (!delta.x && !delta.y) return;

    const currentHorizontal =
      Math.abs(delta.x) >=
      Math.abs(delta.y) *
        numberOption('directionRatio', 1);

    if (
      wheelGesture?.axis === 'horizontal' &&
      wheelGesture.committed
    ) {
      if (currentHorizontal) {
        wheelGesture.lastTime = now;
        preventNativeNavigation(event);
        scheduleWheelReset();
        return;
      }

      resetWheelGesture();
    }

    if (!wheelGesture) {
      wheelGesture = {
        axis: '',
        totalX: 0,
        distanceX: 0,
        distanceY: 0,
        lastTime: now,
        committed: false
      };
    }

    wheelGesture.totalX += delta.x;
    wheelGesture.distanceX += Math.abs(delta.x);
    wheelGesture.distanceY += Math.abs(delta.y);
    wheelGesture.lastTime = now;

    const horizontal = Math.abs(wheelGesture.totalX);
    const horizontalDistance = wheelGesture.distanceX;
    const verticalDistance = wheelGesture.distanceY;
    const ratio = numberOption('directionRatio', 1);
    const lockDistance = numberOption(
      'axisLockDistance',
      1
    );

    if (!wheelGesture.axis) {
      if (Math.max(
        horizontalDistance,
        verticalDistance
      ) >= lockDistance) {
        if (
          horizontalDistance >=
          verticalDistance * ratio
        ) {
          wheelGesture.axis = 'horizontal';
        } else if (
          verticalDistance >=
          horizontalDistance * ratio
        ) {
          wheelGesture.axis = 'vertical';
        }
      }
    }

    scheduleWheelReset();

    if (wheelGesture.axis === 'vertical') return;

    if (!currentHorizontal && !wheelGesture.axis) {
      return;
    }

    preventNativeNavigation(event);

    if (wheelGesture.axis !== 'horizontal') return;

    if (
      horizontal <
      numberOption('trackpadMinimumDistance', 1)
    ) {
      return;
    }

    wheelGesture.committed = true;
    navigate(
      wheelNavigationTarget(wheelGesture.totalX)
    );
  }

  function cancelAllGestures() {
    touchGesture = null;
    endPointerGesture();
    resetWheelGesture();
    suppressClickUntil = 0;
    suppressClickTarget = null;
  }

  function onPageChange() {
    cancelAllGestures();
    syncPageClass();
  }

  function onVisibilityChange() {
    if (document.hidden) cancelAllGestures();
  }

  function destroy() {
    document.removeEventListener(
      'touchstart',
      onTouchStart,
      true
    );
    window.removeEventListener(
      'touchmove',
      onTouchMove,
      true
    );
    window.removeEventListener(
      'touchend',
      onTouchEnd,
      true
    );
    window.removeEventListener(
      'touchcancel',
      onTouchCancel,
      true
    );
    document.removeEventListener(
      'pointerdown',
      onPointerDown,
      true
    );
    window.removeEventListener(
      'pointermove',
      onPointerMove,
      true
    );
    window.removeEventListener(
      'pointerup',
      onPointerUp,
      true
    );
    window.removeEventListener(
      'pointercancel',
      onPointerCancel,
      true
    );
    window.removeEventListener('wheel', onWheel, true);
    document.removeEventListener(
      'dragstart',
      onDragStart,
      true
    );
    document.removeEventListener('click', onClick, true);
    window.removeEventListener('blur', cancelAllGestures);
    window.removeEventListener('pageshow', onPageChange);
    window.removeEventListener('popstate', onPageChange);
    document.removeEventListener(
      'visibilitychange',
      onVisibilityChange
    );
    document.removeEventListener(
      'DOMContentLoaded',
      syncPageClass
    );

    cancelAllGestures();
    document.documentElement.classList.remove(activeClass);
    document.documentElement.classList.remove(draggingClass);
    document.getElementById(styleId)?.remove();

    if (window[globalKey]?.destroy === destroy) {
      delete window[globalKey];
    }
  }

  const passiveCapture = {
    passive: true,
    capture: true
  };
  const activeCapture = {
    passive: false,
    capture: true
  };

  document.addEventListener(
    'touchstart',
    onTouchStart,
    passiveCapture
  );
  window.addEventListener(
    'touchmove',
    onTouchMove,
    activeCapture
  );
  window.addEventListener(
    'touchend',
    onTouchEnd,
    activeCapture
  );
  window.addEventListener(
    'touchcancel',
    onTouchCancel,
    activeCapture
  );
  document.addEventListener(
    'pointerdown',
    onPointerDown,
    passiveCapture
  );
  window.addEventListener(
    'pointermove',
    onPointerMove,
    activeCapture
  );
  window.addEventListener(
    'pointerup',
    onPointerUp,
    activeCapture
  );
  window.addEventListener(
    'pointercancel',
    onPointerCancel,
    passiveCapture
  );
  window.addEventListener(
    'wheel',
    onWheel,
    activeCapture
  );
  document.addEventListener(
    'dragstart',
    onDragStart,
    activeCapture
  );
  document.addEventListener('click', onClick, true);
  window.addEventListener('blur', cancelAllGestures);
  window.addEventListener('pageshow', onPageChange);
  window.addEventListener('popstate', onPageChange);
  document.addEventListener(
    'visibilitychange',
    onVisibilityChange
  );

  if (document.readyState === 'loading') {
    document.addEventListener(
      'DOMContentLoaded',
      syncPageClass
    );
  } else {
    syncPageClass();
  }

  window[globalKey] = {
    destroy,
    get lastNavigationTime() {
      return lastNavigationTime;
    }
  };
})();
</script>

07.26c24v5 Swipe Project Details

#2. Customize

nothing now

#3. Other

#3.1. If you use Personal/Basic Plan and your plan doesn’t support Injection, you can edit Site Footer

05.26c08v1 Event Default Location

Add a Markdown Block

04.26c10v2 Header Sound Icon

Add this code into Markdown

<script>
window.SwipeProjectDetailsOptions = {
  enabled: true,
  touchSwipe: true,
  penSwipe: true,
  mouseDrag: true,
  trackpadSwipe: true,
  minimumDistance: 60,
  trackpadMinimumDistance: 80,
  maximumDuration: 1000,
  axisLockDistance: 10,
  directionRatio: 1.25,
  edgeExclusion: 24,
  trackpadIdleDuration: 180,
  navigationCooldown: 1200,
  trackpadInvertDirection: false,
  swipeLeftTarget: 'previous',
  swipeRightTarget: 'next'
};
</script>
<script>
(() => {
  'use strict';

  const defaults = {
    enabled: true,
    touchSwipe: true,
    penSwipe: true,
    mouseDrag: true,
    trackpadSwipe: true,
    minimumDistance: 60,
    trackpadMinimumDistance: 80,
    maximumDuration: 1000,
    axisLockDistance: 10,
    directionRatio: 1.25,
    edgeExclusion: 24,
    trackpadIdleDuration: 180,
    navigationCooldown: 1200,
    trackpadInvertDirection: false,
    swipeLeftTarget: 'previous',
    swipeRightTarget: 'next'
  };

  const options = {
    ...defaults,
    ...(window.SwipeProjectDetailsOptions || {})
  };

  const globalKey = '__swipeProjectDetails__';
  const styleId = 'swipe-project-details-style';
  const activeClass = 'swipe-project-details-active';
  const draggingClass = 'swipe-project-details-dragging';
  const ignoreSelector = [
    '#itemPagination',
    'button',
    'input',
    'textarea',
    'select',
    'option',
    'label',
    'summary',
    '[contenteditable]:not([contenteditable="false"])',
    '[role="button"]',
    '[role="slider"]',
    '[role="application"]',
    'video',
    'audio',
    'iframe',
    'embed',
    'object',
    'canvas',
    '[data-swipe-navigation-ignore]',
    '.sqs-gallery-design-carousel',
    '.sqs-gallery-design-slideshow',
    '.user-items-list-carousel',
    '[data-controller*="Carousel"]',
    '.swiper',
    '.flickity-enabled',
    '.splide',
    '.glide'
  ].join(',');

  const previousController = window[globalKey];
  let lastNavigationTime =
    Number(previousController?.lastNavigationTime) || 0;

  previousController?.destroy?.();
  document.getElementById(styleId)?.remove();

  const style = document.createElement('style');
  style.id = styleId;
  style.textContent = `
    html.${activeClass},
    html.${activeClass} body {
      overscroll-behavior-x: none;
    }

    html.${draggingClass},
    html.${draggingClass} * {
      -webkit-user-select: none !important;
      user-select: none !important;
      cursor: grabbing !important;
    }
  `;
  (document.head || document.documentElement).appendChild(style);

  let touchGesture = null;
  let pointerGesture = null;
  let wheelGesture = null;
  let wheelTimer = 0;
  let lastTouchTime = 0;
  let suppressClickUntil = 0;
  let suppressClickTarget = null;

  function numberOption(name, minimum) {
    const value = Number(options[name]);
    return Number.isFinite(value)
      ? Math.max(minimum, value)
      : defaults[name];
  }

  function editorActive() {
    return Boolean(
      location.pathname === '/config' ||
      location.pathname.startsWith('/config/') ||
      document.body?.classList.contains('sqs-edit-mode-active') ||
      document.documentElement.classList.contains(
        'sqs-edit-mode-active'
      )
    );
  }

  function pageArea() {
    return (
      document.querySelector('main#page') ||
      document.querySelector('main[role="main"]') ||
      document.querySelector('main')
    );
  }

  function pageActive() {
    return Boolean(
      options.enabled !== false &&
      document.body?.classList.contains('view-item') &&
      document.querySelector('#itemPagination') &&
      !editorActive()
    );
  }

  function syncPageClass() {
    document.documentElement.classList.toggle(
      activeClass,
      pageActive()
    );
  }

  function elementFrom(target) {
    return target instanceof Element
      ? target
      : target?.parentElement || null;
  }

  function hasHorizontalScroller(element, area) {
    let current = element;

    while (current && current !== area) {
      const overflow = getComputedStyle(current).overflowX;

      if (
        /^(auto|scroll|overlay)$/.test(overflow) &&
        current.scrollWidth > current.clientWidth + 2
      ) {
        return true;
      }

      current = current.parentElement;
    }

    return false;
  }

  function allowedTarget(target) {
    syncPageClass();

    const area = pageArea();
    const element = elementFrom(target);

    if (!pageActive() || !area || !element) return null;
    if (!area.contains(element)) return null;
    if (element.closest(ignoreSelector)) return null;
    if (hasHorizontalScroller(element, area)) return null;

    return element;
  }

  function createGesture(x, y, id, type, target, checkEdge) {
    const element = allowedTarget(target);
    if (!element) return null;

    if (checkEdge) {
      const edge = Math.min(
        numberOption('edgeExclusion', 0),
        Math.max(0, window.innerWidth / 2 - 1)
      );

      if (x <= edge || x >= window.innerWidth - edge) {
        return null;
      }
    }

    return {
      id,
      type,
      target: element,
      captureTarget: null,
      axis: '',
      startX: x,
      startY: y,
      lastX: x,
      lastY: y,
      startTime: Date.now(),
      dragging: false
    };
  }

  function updateGesture(gesture, x, y) {
    gesture.lastX = x;
    gesture.lastY = y;

    if (gesture.axis) return gesture.axis;

    const horizontal = Math.abs(x - gesture.startX);
    const vertical = Math.abs(y - gesture.startY);
    const lockDistance = numberOption('axisLockDistance', 1);
    const ratio = numberOption('directionRatio', 1);

    if (Math.max(horizontal, vertical) < lockDistance) {
      return '';
    }

    if (horizontal >= vertical * ratio) {
      gesture.axis = 'horizontal';
    } else if (vertical >= horizontal * ratio) {
      gesture.axis = 'vertical';
    }

    return gesture.axis;
  }

  function navigationTarget(gesture, x, y) {
    const horizontal = x - gesture.startX;
    const vertical = y - gesture.startY;
    const horizontalDistance = Math.abs(horizontal);
    const verticalDistance = Math.abs(vertical);
    const elapsed = Date.now() - gesture.startTime;

    if (
      horizontalDistance < numberOption('minimumDistance', 1) ||
      elapsed > numberOption('maximumDuration', 1) ||
      horizontalDistance <
        verticalDistance * numberOption('directionRatio', 1)
    ) {
      return '';
    }

    return horizontal < 0
      ? options.swipeLeftTarget
      : options.swipeRightTarget;
  }

  function paginationLink(target) {
    if (target === 'previous') {
      return document.querySelector(
        '#itemPagination .item-pagination-link--prev'
      );
    }

    if (target === 'next') {
      return document.querySelector(
        '#itemPagination .item-pagination-link--next'
      );
    }

    return null;
  }

  function preventNativeNavigation(event) {
    if (event?.cancelable) event.preventDefault();
  }

  function navigate(target) {
    const now = Date.now();
    const cooldown = numberOption('navigationCooldown', 0);

    if (now - lastNavigationTime < cooldown) return false;

    const link = paginationLink(target);
    if (!link?.href) return false;

    lastNavigationTime = now;
    link.click();
    return true;
  }

  function completeGesture(gesture, x, y, event) {
    const target = navigationTarget(gesture, x, y);
    if (!target) return false;

    preventNativeNavigation(event);
    navigate(target);
    return true;
  }

  function findTouch(list, id) {
    return Array.from(list || []).find(
      (touch) => touch.identifier === id
    );
  }

  function onTouchStart(event) {
    lastTouchTime = Date.now();

    if (
      options.touchSwipe === false ||
      event.touches.length !== 1
    ) {
      touchGesture = null;
      return;
    }

    const touch = event.touches[0];
    touchGesture = createGesture(
      touch.clientX,
      touch.clientY,
      touch.identifier,
      'touch',
      event.target,
      true
    );
  }

  function onTouchMove(event) {
    lastTouchTime = Date.now();
    if (!touchGesture) return;

    if (event.touches.length !== 1) {
      touchGesture = null;
      return;
    }

    const touch = findTouch(
      event.touches,
      touchGesture.id
    );

    if (!touch) {
      touchGesture = null;
      return;
    }

    const axis = updateGesture(
      touchGesture,
      touch.clientX,
      touch.clientY
    );

    if (axis === 'vertical') {
      touchGesture = null;
    } else if (axis === 'horizontal') {
      preventNativeNavigation(event);
    }
  }

  function onTouchEnd(event) {
    lastTouchTime = Date.now();
    if (!touchGesture) return;

    const gesture = touchGesture;
    const touch = findTouch(event.changedTouches, gesture.id);
    touchGesture = null;

    if (touch) {
      completeGesture(
        gesture,
        touch.clientX,
        touch.clientY,
        event
      );
    }
  }

  function onTouchCancel(event) {
    lastTouchTime = Date.now();
    touchGesture = null;
  }

  function pointerAllowed(event) {
    if (event.isPrimary === false || event.button !== 0) {
      return false;
    }

    if (event.pointerType === 'mouse') {
      return Boolean(
        options.mouseDrag &&
        Date.now() - lastTouchTime > 800
      );
    }

    if (event.pointerType === 'pen') {
      return options.penSwipe !== false;
    }

    return false;
  }

  function setPointerCapture(gesture, target) {
    if (!(target instanceof Element)) return;

    try {
      target.setPointerCapture?.(gesture.id);
      gesture.captureTarget = target;
    } catch {
      gesture.captureTarget = null;
    }
  }

  function releasePointerCapture(gesture) {
    const target = gesture?.captureTarget;
    if (!target) return;

    try {
      if (target.hasPointerCapture?.(gesture.id)) {
        target.releasePointerCapture(gesture.id);
      }
    } catch {
      return;
    }
  }

  function beginMousePresentation(gesture) {
    if (gesture.dragging) return;

    gesture.dragging = true;
    document.documentElement.classList.add(draggingClass);
    window.getSelection?.()?.removeAllRanges?.();
  }

  function endPointerGesture() {
    releasePointerCapture(pointerGesture);
    pointerGesture = null;
    document.documentElement.classList.remove(draggingClass);
  }

  function onPointerDown(event) {
    if (!pointerAllowed(event)) return;

    endPointerGesture();
    pointerGesture = createGesture(
      event.clientX,
      event.clientY,
      event.pointerId,
      event.pointerType,
      event.target,
      false
    );

    if (pointerGesture) {
      setPointerCapture(pointerGesture, event.target);
    }
  }

  function onPointerMove(event) {
    if (
      !pointerGesture ||
      pointerGesture.id !== event.pointerId ||
      pointerGesture.type !== event.pointerType
    ) {
      return;
    }

    const axis = updateGesture(
      pointerGesture,
      event.clientX,
      event.clientY
    );

    if (axis === 'vertical') {
      endPointerGesture();
      return;
    }

    if (axis === 'horizontal') {
      beginMousePresentation(pointerGesture);
      preventNativeNavigation(event);
    }
  }

  function onPointerUp(event) {
    if (
      !pointerGesture ||
      pointerGesture.id !== event.pointerId ||
      pointerGesture.type !== event.pointerType
    ) {
      return;
    }

    const gesture = pointerGesture;
    updateGesture(gesture, event.clientX, event.clientY);
    const completed = completeGesture(
      gesture,
      event.clientX,
      event.clientY,
      event
    );

    if (completed) {
      suppressClickUntil = Date.now() + 500;
      suppressClickTarget = gesture.target;
    }

    endPointerGesture();
  }

  function onPointerCancel(event) {
    if (
      !pointerGesture ||
      pointerGesture.id !== event.pointerId ||
      pointerGesture.type !== event.pointerType
    ) {
      return;
    }

    endPointerGesture();
  }

  function onDragStart(event) {
    if (
      pointerGesture?.type === 'mouse' &&
      allowedTarget(event.target)
    ) {
      preventNativeNavigation(event);
    }
  }

  function onClick(event) {
    const target = elementFrom(event.target);
    const source = suppressClickTarget;
    const matchingTarget = Boolean(
      source &&
      target &&
      (
        source === target ||
        source.contains(target) ||
        target.contains(source)
      )
    );

    if (
      event.isTrusted &&
      Date.now() < suppressClickUntil &&
      matchingTarget
    ) {
      suppressClickUntil = 0;
      suppressClickTarget = null;
      event.preventDefault();
      event.stopImmediatePropagation();
    }
  }

  function normalizeWheelDelta(event) {
    const multiplier =
      event.deltaMode === 1
        ? 16
        : event.deltaMode === 2
          ? Math.max(window.innerWidth, 1)
          : 1;

    return {
      x: event.deltaX * multiplier,
      y: event.deltaY * multiplier
    };
  }

  function resetWheelGesture() {
    clearTimeout(wheelTimer);
    wheelTimer = 0;
    wheelGesture = null;
  }

  function scheduleWheelReset() {
    clearTimeout(wheelTimer);
    wheelTimer = window.setTimeout(
      resetWheelGesture,
      numberOption('trackpadIdleDuration', 1)
    );
  }

  function wheelNavigationTarget(horizontal) {
    const leftward = options.trackpadInvertDirection
      ? horizontal < 0
      : horizontal > 0;

    return leftward
      ? options.swipeLeftTarget
      : options.swipeRightTarget;
  }

  function onWheel(event) {
    const now = Date.now();
    const idleDuration = numberOption(
      'trackpadIdleDuration',
      1
    );

    if (
      wheelGesture &&
      now - wheelGesture.lastTime > idleDuration
    ) {
      resetWheelGesture();
    }

    if (
      options.trackpadSwipe === false ||
      event.ctrlKey ||
      event.shiftKey ||
      !allowedTarget(event.target)
    ) {
      resetWheelGesture();
      return;
    }

    const delta = normalizeWheelDelta(event);
    if (!delta.x && !delta.y) return;

    const currentHorizontal =
      Math.abs(delta.x) >=
      Math.abs(delta.y) *
        numberOption('directionRatio', 1);

    if (
      wheelGesture?.axis === 'horizontal' &&
      wheelGesture.committed
    ) {
      if (currentHorizontal) {
        wheelGesture.lastTime = now;
        preventNativeNavigation(event);
        scheduleWheelReset();
        return;
      }

      resetWheelGesture();
    }

    if (!wheelGesture) {
      wheelGesture = {
        axis: '',
        totalX: 0,
        distanceX: 0,
        distanceY: 0,
        lastTime: now,
        committed: false
      };
    }

    wheelGesture.totalX += delta.x;
    wheelGesture.distanceX += Math.abs(delta.x);
    wheelGesture.distanceY += Math.abs(delta.y);
    wheelGesture.lastTime = now;

    const horizontal = Math.abs(wheelGesture.totalX);
    const horizontalDistance = wheelGesture.distanceX;
    const verticalDistance = wheelGesture.distanceY;
    const ratio = numberOption('directionRatio', 1);
    const lockDistance = numberOption(
      'axisLockDistance',
      1
    );

    if (!wheelGesture.axis) {
      if (Math.max(
        horizontalDistance,
        verticalDistance
      ) >= lockDistance) {
        if (
          horizontalDistance >=
          verticalDistance * ratio
        ) {
          wheelGesture.axis = 'horizontal';
        } else if (
          verticalDistance >=
          horizontalDistance * ratio
        ) {
          wheelGesture.axis = 'vertical';
        }
      }
    }

    scheduleWheelReset();

    if (wheelGesture.axis === 'vertical') return;

    if (!currentHorizontal && !wheelGesture.axis) {
      return;
    }

    preventNativeNavigation(event);

    if (wheelGesture.axis !== 'horizontal') return;

    if (
      horizontal <
      numberOption('trackpadMinimumDistance', 1)
    ) {
      return;
    }

    wheelGesture.committed = true;
    navigate(
      wheelNavigationTarget(wheelGesture.totalX)
    );
  }

  function cancelAllGestures() {
    touchGesture = null;
    endPointerGesture();
    resetWheelGesture();
    suppressClickUntil = 0;
    suppressClickTarget = null;
  }

  function onPageChange() {
    cancelAllGestures();
    syncPageClass();
  }

  function onVisibilityChange() {
    if (document.hidden) cancelAllGestures();
  }

  function destroy() {
    document.removeEventListener(
      'touchstart',
      onTouchStart,
      true
    );
    window.removeEventListener(
      'touchmove',
      onTouchMove,
      true
    );
    window.removeEventListener(
      'touchend',
      onTouchEnd,
      true
    );
    window.removeEventListener(
      'touchcancel',
      onTouchCancel,
      true
    );
    document.removeEventListener(
      'pointerdown',
      onPointerDown,
      true
    );
    window.removeEventListener(
      'pointermove',
      onPointerMove,
      true
    );
    window.removeEventListener(
      'pointerup',
      onPointerUp,
      true
    );
    window.removeEventListener(
      'pointercancel',
      onPointerCancel,
      true
    );
    window.removeEventListener('wheel', onWheel, true);
    document.removeEventListener(
      'dragstart',
      onDragStart,
      true
    );
    document.removeEventListener('click', onClick, true);
    window.removeEventListener('blur', cancelAllGestures);
    window.removeEventListener('pageshow', onPageChange);
    window.removeEventListener('popstate', onPageChange);
    document.removeEventListener(
      'visibilitychange',
      onVisibilityChange
    );
    document.removeEventListener(
      'DOMContentLoaded',
      syncPageClass
    );

    cancelAllGestures();
    document.documentElement.classList.remove(activeClass);
    document.documentElement.classList.remove(draggingClass);
    document.getElementById(styleId)?.remove();

    if (window[globalKey]?.destroy === destroy) {
      delete window[globalKey];
    }
  }

  const passiveCapture = {
    passive: true,
    capture: true
  };
  const activeCapture = {
    passive: false,
    capture: true
  };

  document.addEventListener(
    'touchstart',
    onTouchStart,
    passiveCapture
  );
  window.addEventListener(
    'touchmove',
    onTouchMove,
    activeCapture
  );
  window.addEventListener(
    'touchend',
    onTouchEnd,
    activeCapture
  );
  window.addEventListener(
    'touchcancel',
    onTouchCancel,
    activeCapture
  );
  document.addEventListener(
    'pointerdown',
    onPointerDown,
    passiveCapture
  );
  window.addEventListener(
    'pointermove',
    onPointerMove,
    activeCapture
  );
  window.addEventListener(
    'pointerup',
    onPointerUp,
    activeCapture
  );
  window.addEventListener(
    'pointercancel',
    onPointerCancel,
    passiveCapture
  );
  window.addEventListener(
    'wheel',
    onWheel,
    activeCapture
  );
  document.addEventListener(
    'dragstart',
    onDragStart,
    activeCapture
  );
  document.addEventListener('click', onClick, true);
  window.addEventListener('blur', cancelAllGestures);
  window.addEventListener('pageshow', onPageChange);
  window.addEventListener('popstate', onPageChange);
  document.addEventListener(
    'visibilitychange',
    onVisibilityChange
  );

  if (document.readyState === 'loading') {
    document.addEventListener(
      'DOMContentLoaded',
      syncPageClass
    );
  } else {
    syncPageClass();
  }

  window[globalKey] = {
    destroy,
    get lastNavigationTime() {
      return lastNavigationTime;
    }
  };
})();
</script>

07.26c24v5 Swipe Project Details

 

Buy me a coffee