Add initial project

This commit is contained in:
vollstock committed 2026-10-02 15:55:28 +02:00
1 parent b3ad230739
commit a79d639017
6 files changed
+1084

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
"NO ASSHOLE-WARE LICENSE"
<daniel.stock@tooloop.de> wrote this file. As long as you retain this notice you
can do whatever you want with this stuff. If we meet some day, and you think
this stuff is worth it, you can buy me a beer.
Be nice to other people. Respect women. Don't be a dick. Don't support fascists.
If you can't do that, don't use this code.
+175
View File
@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snappy</title>
<style>
html,
body {
/* Transitional from https://modernfontstacks.com/ */
font-family: Charter, 'Bitstream Charter', 'Sitka Text', Cambria, serif;
font-weight: normal;
font-size: 1.25rem;
background-color: #f8f9fa;
color: #343a40;
@media (prefers-color-scheme: dark) {
background-color: #212529;
color: #f8f9fa;
}
}
body {
margin: 2rem;
}
h1 {
font-size: 3rem;
margin-bottom: 1em;
}
.snappy-carousel {
counter-reset: snappy-slide;
max-height: 300px;
}
.snappy-slide {
counter-increment: snappy-slide;
display: flex;
justify-content: center;
align-items: center;
width: 33%;
aspect-ratio: 3/2;
max-height: 400px;
background-color: #f1f3f5;
color: #343a40;
@media (prefers-color-scheme: dark) {
background-color: #495057;
color: #f8f9fa;
}
}
.snappy-slide:nth-child(even) {
background-color: #e9ecef;
@media (prefers-color-scheme: dark) {
background-color: #343a40;
color: #f8f9fa;
}
}
.snappy-slide:before {
content: counter(snappy-slide);
}
.snappy-controls {
display: flex;
align-items: center;
gap: 0.25rem;
margin-top: 1rem;
}
.snappy-button {
width: 1.5rem;
height: 1.5rem;
font-size: 1rem;
border: none;
background-color: #f1f3f5;
color: #339af0;
&:hover,
&:active {
background-color: #dee2e6;
}
@media (prefers-color-scheme: dark) {
background-color: #495057;
&:hover,
&:active {
background-color: #868e96;
color: #1971c2;
}
}
}
.snappy-pagination {
display: flex;
}
.snappy-pagination-dot {
width: 0.66rem !important;
height: 0.66rem !important;
border-radius: 0.33rem;
background-color: #dee2e6 !important;
margin: 0 0.25rem;
transition-property: width, background;
transition-duration: .8s;
transition-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
cursor: pointer;
}
.snappy-pagination-dot:hover {
background-color: #adb5bd !important;
}
.snappy-pagination-dot.is-active {
background-color: #1971c2 !important;
width: 2rem !important;
}
</style>
<link rel="stylesheet" href="dist/snappy.css">
</head>
<body>
<h1>Snappy</h1>
<!--
Options
-------
https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll_snap
data-direction="x"
data-direction="y"
data-gap="var(--size-1)"
data-gap="10px"
data-gap="1rem"
data-snap-stop="always"
data-snap-stop="normal"
data-snap-align="center"
data-snap-align="start"
data-snap-align="end"
-->
<div id="demo-carousel" class="snappy-carousel" data-gap="1rem" data-snap-align="start">
<div class="snappy-slide"></div>
<div class="snappy-slide"></div>
<div class="snappy-slide"></div>
<div class="snappy-slide"></div>
<div class="snappy-slide"></div>
<div class="snappy-slide"></div>
<div class="snappy-slide"></div>
<div class="snappy-slide"></div>
</div>
<div class="snappy-controls">
<button class="snappy-button snappy-prev" data-carousel="demo-carousel">❮</button>
<button class="snappy-button snappy-next" data-carousel="demo-carousel">❯</button>
<div class="snappy-pagination" data-carousel="demo-carousel"></div>
</div>
<!-- Javascript is only needed for pagination and buttons -->
<script src="dist/snappy.js" defer></script>
</body>
</html>
+499
View File
@@ -0,0 +1,499 @@
{
"name": "snappy-carousel",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "snappy-carousel",
"version": "1.0.0",
"devDependencies": {
"esbuild": "^0.25.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "snappy-carousel",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "esbuild src/snappy.js src/snappy.css --outdir=dist --minify",
"watch": "esbuild src/snappy.js src/snappy.css --outdir=dist --minify --watch"
},
"devDependencies": {
"esbuild": "^0.25.0"
}
}
+57
View File
@@ -0,0 +1,57 @@
.snappy-carousel {
display: flex;
flex-direction: row;
overflow-x: auto;
overflow-y: hidden;
user-select: none;
cursor: grab;
scroll-behavior: smooth;
&[data-direction="y"] {
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
}
&.is-dragging {
scroll-snap-type: none;
scroll-behavior: unset;
}
scroll-snap-type: x mandatory;
&[data-direction="y"] {
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
scroll-snap-type: y mandatory;
}
gap: var(--carousel-gap, 0);
gap: attr(data-gap type(*), 0);
.snappy-slide {
flex-shrink: 0;
scroll-snap-align: center;
scroll-snap-stop: normal;
}
&[data-snap-align="start"] .snappy-slide {
scroll-snap-align: start;
}
&[data-snap-align="end"] .snappy-slide {
scroll-snap-align: end;
}
&[data-snap-stop="always"] .snappy-slide {
scroll-snap-stop: always;
}
}
.snappy-pagination-dot {
display: inline-block;
width: 1rem;
height: 1rem;
background-color: silver;
}
+334
View File
@@ -0,0 +1,334 @@
/* use strict; */
document.addEventListener('DOMContentLoaded', () => {
window.snappy = new Snappy();
});
class Snappy {
carousels = [];
constructor() {
document.querySelectorAll('.snappy-carousel').forEach(element => {
this.carousels.push(new Carousel(element));
});
}
}
class Carousel {
// DOM references
element = null;
prevButton = null;
nextButton = null;
pagination = null;
slides = [];
paginationDots = [];
// Bahavioral constants
inertia = 0.90; // Decay rate (0.9 = slower stop, 0.99 = longer slide)
// State variables
isDragging = false;
direction = 'x';
startPosition = 0;
startScrollPosition = 0;
velocity = 0;
lastPosition;
lastTime;
activeSlideIndex = 0;
constructor(element) {
this.element = element;
this.direction = this.element.dataset.direction === 'y' ? 'y' : 'x';
const gap = this.element.dataset.gap;
if (gap) this.element.style.setProperty('--carousel-gap', gap);
this.slides = this.element.querySelectorAll('.snappy-slide');
this.prevButton = document.querySelector(`.snappy-prev[data-carousel="${this.element.id}"]`);
this.nextButton = document.querySelector(`.snappy-next[data-carousel="${this.element.id}"]`);
this.pagination = document.querySelector(`.snappy-pagination[data-carousel="${this.element.id}"]`);
this.element.addEventListener('mousedown', this.startDrag.bind(this));
this.element.addEventListener('mousemove', this.drag.bind(this));
this.element.addEventListener('mouseup', this.endDrag.bind(this));
this.element.addEventListener('mouseleave', this.endDrag.bind(this));
this.element.addEventListener('touchstart', this.reset.bind(this));
this.element.addEventListener('wheel', this.reset.bind(this));
this.element.addEventListener('scroll', this.updateActiveSlide.bind(this));
if (this.prevButton) {
this.prevButton.addEventListener('click', () => this.prev());
}
if (this.nextButton) {
this.nextButton.addEventListener('click', () => this.next());
}
if (this.pagination) {
this.createPagination();
}
this.updateActiveSlide();
}
startDrag(e) {
this.isDragging = true;
this.element.classList.add('is-dragging'); // Disable scroll snapping
this.velocity = 0;
this.lastPosition = undefined;
this.lastTime = undefined;
this.startPosition = this.getPointerPosition(e);
this.startScrollPosition = this.getScrollPosition();
e.preventDefault(); // Prevent text selection during drag
}
drag(e) {
if (!this.isDragging) return;
e.preventDefault();
const pointerPosition = this.getPointerPosition(e);
const currentTime = Date.now();
// Calculate velocity (pixels per millisecond)
if (this.lastTime !== undefined && currentTime > this.lastTime) {
this.velocity = (pointerPosition - this.lastPosition) / (currentTime - this.lastTime);
}
this.lastPosition = pointerPosition;
this.lastTime = currentTime;
const distance = pointerPosition - this.startPosition;
requestAnimationFrame(() => {
this.setScrollPosition(this.startScrollPosition - distance);
});
}
endDrag() {
if (!this.isDragging) return;
this.isDragging = false;
// If the drag velocity is low at release, let the native scroll snap take over
if (Math.abs(this.velocity) < 0.05) {
this.reset();
return;
}
const target = this.calculateDragTarget();
const element = this.element;
const distance = target - this.getScrollPosition();
this.velocity = -distance * (1 - this.inertia) / 10;
const animateInertia = () => {
if (this.isDragging) return; // Stop the animation if the user starts dragging again
const current = this.getScrollPosition();
const remaining = target - current;
if (Math.abs(remaining) <= 0.1) {
this.finishDrag(target);
return;
}
const delta = -this.velocity * 10;
const next = Math.abs(delta) >= Math.abs(remaining)
? target
: current + delta;
this.setScrollPosition(next);
// Stop if the browser cannot represent a smaller scroll-position change.
if (this.getScrollPosition() === current || this.getScrollPosition() === target) {
this.finishDrag(target);
return;
}
this.velocity *= this.inertia;
requestAnimationFrame(animateInertia);
};
animateInertia();
}
finishDrag(target) {
this.setScrollPosition(target);
this.reset();
}
getPointerPosition(event) {
return this.direction === 'y' ? event.clientY : event.clientX;
}
getScrollPosition() {
return this.direction === 'y' ? this.element.scrollTop : this.element.scrollLeft;
}
setScrollPosition(position) {
if (this.direction === 'y') {
this.element.scrollTop = position;
} else {
this.element.scrollLeft = position;
}
}
calculateDragTarget() {
const element = this.element;
const isVertical = this.direction === 'y';
const maxScrollPosition = isVertical
? element.scrollHeight - element.clientHeight
: element.scrollWidth - element.clientWidth;
const clamp = value => Math.max(0, Math.min(value, maxScrollPosition));
const cssPixels = value => Number.parseFloat(value) || 0;
const startEdge = isVertical ? 'top' : 'left';
const endEdge = isVertical ? 'bottom' : 'right';
const paddingStart = isVertical ? 'scrollPaddingTop' : 'scrollPaddingLeft';
const paddingEnd = isVertical ? 'scrollPaddingBottom' : 'scrollPaddingRight';
const marginStart = isVertical ? 'scrollMarginTop' : 'scrollMarginLeft';
const marginEnd = isVertical ? 'scrollMarginBottom' : 'scrollMarginRight';
// Project the final scroll position based on current velocity and inertia
const projectedStop = clamp(
this.getScrollPosition() - this.velocity * 10 / (1 - this.inertia)
);
if (!this.slides.length) return clamp(projectedStop);
const elementRect = element.getBoundingClientRect();
const elementStyle = getComputedStyle(element);
const snapportStartEdge =
elementRect[startEdge] + (isVertical ? element.clientTop : element.clientLeft);
const clientExtent = isVertical ? element.clientHeight : element.clientWidth;
const snapportStart =
snapportStartEdge + cssPixels(elementStyle[paddingStart]);
const snapportEnd =
snapportStartEdge + clientExtent - cssPixels(elementStyle[paddingEnd]);
const targets = [...this.slides].map(slide => {
const slideRect = slide.getBoundingClientRect();
const slideStyle = getComputedStyle(slide);
const snapAreaStart =
slideRect[startEdge] - cssPixels(slideStyle[marginStart]);
const snapAreaEnd =
slideRect[endEdge] + cssPixels(slideStyle[marginEnd]);
const alignments = slideStyle.scrollSnapAlign.trim().split(/\s+/);
const align = isVertical ? alignments[0] : alignments[1] ?? alignments[0];
const scrollPosition = this.getScrollPosition();
let target;
if (align === 'start') {
target = scrollPosition + snapAreaStart - snapportStart;
} else if (align === 'end') {
target = scrollPosition + snapAreaEnd - snapportEnd;
} else {
const snapAreaCenter = (snapAreaStart + snapAreaEnd) / 2;
const snapportCenter = (snapportStart + snapportEnd) / 2;
target = scrollPosition + snapAreaCenter - snapportCenter;
}
return clamp(target);
});
return targets.reduce((nearest, target) =>
Math.abs(target - projectedStop) < Math.abs(nearest - projectedStop)
? target
: nearest
);
}
reset() {
this.isDragging = false;
this.element.classList.remove('is-dragging');
this.velocity = 0
}
createPagination() {
this.slides.forEach((slide, index) => {
// Populate the pagination container with dots for each slide
let div = document.createElement('div');
div.classList.add('snappy-pagination-dot');
div.addEventListener('click', () => this.goToSlide(index));
this.pagination.appendChild(div);
// Store a reference to the pagination dots in the carousel object for later use
if (!this.paginationDots) this.paginationDots = [];
this.paginationDots.push(div);
});
}
getActiveSlideIndex() {
if (!this.slides.length) return -1;
const isVertical = this.direction === 'y';
const startEdge = isVertical ? 'top' : 'left';
const endEdge = isVertical ? 'bottom' : 'right';
const paddingStart = isVertical ? 'scrollPaddingTop' : 'scrollPaddingLeft';
const paddingEnd = isVertical ? 'scrollPaddingBottom' : 'scrollPaddingRight';
const marginStart = isVertical ? 'scrollMarginTop' : 'scrollMarginLeft';
const marginEnd = isVertical ? 'scrollMarginBottom' : 'scrollMarginRight';
const cssPixels = value => Number.parseFloat(value) || 0;
const containerRect = this.element.getBoundingClientRect();
const containerStyle = getComputedStyle(this.element);
const snapportStartEdge = containerRect[startEdge] +
(isVertical ? this.element.clientTop : this.element.clientLeft);
const clientExtent = isVertical ? this.element.clientHeight : this.element.clientWidth;
const snapportStart = snapportStartEdge + cssPixels(containerStyle[paddingStart]);
const snapportEnd = snapportStartEdge + clientExtent - cssPixels(containerStyle[paddingEnd]);
const snapportCenter = (snapportStart + snapportEnd) / 2;
let activeIndex = 0;
let closestDistance = Infinity;
this.slides.forEach((slide, index) => {
const rect = slide.getBoundingClientRect();
const slideStyle = getComputedStyle(slide);
const snapAreaStart = rect[startEdge] - cssPixels(slideStyle[marginStart]);
const snapAreaEnd = rect[endEdge] + cssPixels(slideStyle[marginEnd]);
const alignments = slideStyle.scrollSnapAlign.trim().split(/\s+/);
const align = isVertical ? alignments[0] : alignments[1] ?? alignments[0];
let distance;
if (align === 'start') {
distance = Math.abs(snapAreaStart - snapportStart);
} else if (align === 'end') {
distance = Math.abs(snapAreaEnd - snapportEnd);
} else {
const snapAreaCenter = (snapAreaStart + snapAreaEnd) / 2;
distance = Math.abs(snapAreaCenter - snapportCenter);
}
if (distance < closestDistance) {
activeIndex = index;
closestDistance = distance;
}
});
return activeIndex;
}
updateActiveSlide() {
this.activeSlideIndex = this.getActiveSlideIndex();
this.slides.forEach((slide, index) => {
slide.classList.toggle('is-active', index === this.activeSlideIndex);
});
this.paginationDots.forEach((dot, index) => {
dot.classList.toggle('is-active', index === this.activeSlideIndex);
});
}
goToSlide(index) {
this.activeSlideIndex = Math.max(0, Math.min(index, this.slides.length - 1));
this.slides[this.activeSlideIndex].scrollIntoView();
}
next() {
this.goToSlide(this.activeSlideIndex + 1);
}
prev() {
this.goToSlide(this.activeSlideIndex - 1);
}
}