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

#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>

#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

Add a Markdown Block
![]()
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>
