WIP
This commit is contained in:
21
resources/js/filepond.js
Normal file
21
resources/js/filepond.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as FilePond from 'filepond';
|
||||
import FilePondPluginImageExifOrientation from 'filepond-plugin-image-exif-orientation';
|
||||
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
|
||||
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size';
|
||||
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
|
||||
|
||||
FilePond.registerPlugin(FilePondPluginImageExifOrientation);
|
||||
FilePond.registerPlugin(FilePondPluginImagePreview);
|
||||
FilePond.registerPlugin(FilePondPluginFileValidateSize);
|
||||
FilePond.registerPlugin(FilePondPluginFileValidateType);
|
||||
|
||||
window.FilePond = FilePond;
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.store('uploader', {
|
||||
states: {},
|
||||
setState(state, value) {
|
||||
this.states[state] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
76
resources/js/lightbox.js
Normal file
76
resources/js/lightbox.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||
|
||||
document.addEventListener('livewire:navigated', () => {
|
||||
const lightbox = new PhotoSwipeLightbox({
|
||||
gallery: '#album',
|
||||
children: 'a',
|
||||
bgOpacity: 1,
|
||||
arrowPrevSVG: '<svg class="w-12 h-12 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 19-7-7 7-7"/></svg>',
|
||||
arrowNextSVG: '<svg class="w-12 h-12 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 5 7 7-7 7"/></svg>',
|
||||
closeSVG: '<div class="bg-transparent p-2 inline-flex items-center me-2 hover:bg-gray-100 rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700"><svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/></svg></div>',
|
||||
zoomSVG: '<div class="bg-transparent p-2 inline-flex items-center me-2 hover:bg-gray-100 rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700"><svg class="pswp__zoom-icn-bar-v w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="m21 21-3.5-3.5M10 7v6m-3-3h6m4 0a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"/></svg><svg class="pswp__zoom-icn-bar-b hidden w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="m21 21-3.5-3.5M7 10h6m4 0a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"/></svg></div>',
|
||||
pswpModule: () => import('photoswipe')
|
||||
});
|
||||
|
||||
lightbox.on('uiRegister', function() {
|
||||
lightbox.pswp.ui.registerElement({
|
||||
name: 'download-button',
|
||||
order: 9,
|
||||
isButton: true,
|
||||
tagName: 'button',
|
||||
html: '<div class="bg-transparent p-2 inline-flex items-center me-2 hover:bg-gray-100 rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700"><svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 13V4M7 14H5a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1h-2m-1-5-4 5-4-5m9 8h.01"/></svg></div>',
|
||||
|
||||
onInit: (el, pswp) => {
|
||||
el.setAttribute('download', '');
|
||||
el.setAttribute('target', '_blank');
|
||||
el.setAttribute('rel', 'noopener');
|
||||
|
||||
pswp.on('change', () => {
|
||||
el.href = pswp.currSlide.data.element.dataset.pswpDownload;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
lightbox.pswp.ui.registerElement({
|
||||
name: 'rotate-button-cw',
|
||||
ariaLabel: 'Rotate clockwise',
|
||||
order: 8,
|
||||
isButton: true,
|
||||
html: '<div class="bg-transparent p-2 inline-flex items-center me-2 hover:bg-gray-100 rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700"><svg xmlns="http://www.w3.org/2000/svg" fill="none" aria-hidden="true" class="w-6 h-6 text-gray-800 dark:text-white" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><path d="M19.407 11.376a7.552 7.552 0 1 1-2.212-5.34"/><path d="M17.555 2.612v4h-4"/></svg></div>',
|
||||
onClick: (event, el) => {
|
||||
pswp.close();
|
||||
Livewire.dispatch(`image.rotate`, {
|
||||
image_id: pswp.currSlide.data.element.dataset.id,
|
||||
direction: 'cw'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
lightbox.pswp.ui.registerElement({
|
||||
name: 'rotate-button-ccw',
|
||||
ariaLabel: 'Rotate counter-clockwise',
|
||||
order: 7,
|
||||
isButton: true,
|
||||
html: '<div class="bg-transparent p-2 inline-flex items-center me-2 hover:bg-gray-100 rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700"><svg xmlns="http://www.w3.org/2000/svg" fill="none" aria-hidden="true" class="w-6 h-6 text-gray-800 dark:text-white scale-x-[-1]" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><path d="M19.407 11.376a7.552 7.552 0 1 1-2.212-5.34"/><path d="M17.555 2.612v4h-4"/></svg></div>',
|
||||
onClick: (event, el) => {
|
||||
pswp.close();
|
||||
Livewire.dispatch(`image.rotate`, {
|
||||
image_id: pswp.currSlide.data.element.dataset.id,
|
||||
direction: 'cw'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
lightbox.init();
|
||||
|
||||
document.addEventListener('livewire:init', () => {
|
||||
Livewire.hook('morph.added', ({ el }) => {
|
||||
if(el.dataset.pswpWidth !== undefined) {
|
||||
if(lightbox.pswp !== undefined) {
|
||||
lightbox.pswp.options.dataSource.items.push(el);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
144
resources/js/notification.js
Normal file
144
resources/js/notification.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import {renderToaster, renderToast} from './notification/ui';
|
||||
import registerSwipe from './notification/touch';
|
||||
|
||||
const VISIBLE_TOASTS_AMOUNT = 3;
|
||||
const TOAST_LIFETIME = 4000;
|
||||
const GAP = 14;
|
||||
const TIME_BEFORE_UNMOUNT = 200;
|
||||
|
||||
window.ToastinTakin = {
|
||||
init() {
|
||||
if (reinitializeToaster()) {
|
||||
return;
|
||||
}
|
||||
renderToaster();
|
||||
|
||||
const toaster = document.getElementById("toaster");
|
||||
registerMouseOver(toaster);
|
||||
registerKeyboardShortcuts(toaster);
|
||||
},
|
||||
success(msg) {
|
||||
ToastinTakin.show(msg, "success");
|
||||
},
|
||||
error(msg) {
|
||||
ToastinTakin.show(msg, "error");
|
||||
},
|
||||
info(msg) {
|
||||
ToastinTakin.show(msg, "info");
|
||||
},
|
||||
warning(msg) {
|
||||
ToastinTakin.show(msg, "warning");
|
||||
},
|
||||
show(msg, type) {
|
||||
const list = document.getElementById("toaster");
|
||||
const { toast, id } = renderToast(list, msg, type);
|
||||
|
||||
const el = list.children[0];
|
||||
const height = el.getBoundingClientRect().height;
|
||||
|
||||
el.dataset.initialHeight = height;
|
||||
el.dataset.hidden = false;
|
||||
list.style.setProperty("--front-toast-height", `${height}px`);
|
||||
|
||||
registerSwipe(id);
|
||||
refreshProperties();
|
||||
if(list.dataset.expanded === "false") {
|
||||
registerRemoveTimeout(el);
|
||||
}
|
||||
},
|
||||
remove(id) {
|
||||
const el = document.querySelector(`[data-toast-id="${id}"]`);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
el.dataset.removed = true;
|
||||
|
||||
refreshProperties();
|
||||
|
||||
const previousTid = el.dataset.unmountTid;
|
||||
if (previousTid) window.clearTimeout(previousTid);
|
||||
|
||||
const tid = window.setTimeout(function () {
|
||||
el.parentElement?.removeChild(el);
|
||||
}, TIME_BEFORE_UNMOUNT);
|
||||
el.dataset.unmountTid = tid;
|
||||
},
|
||||
};
|
||||
|
||||
function registerRemoveTimeout(el) {
|
||||
const tid = window.setTimeout(function () {
|
||||
ToastinTakin.remove(el.dataset.toastId);
|
||||
}, TOAST_LIFETIME);
|
||||
el.dataset.removeTid = tid;
|
||||
}
|
||||
|
||||
function reinitializeToaster() {
|
||||
const ol = document.getElementById("toaster");
|
||||
if (!ol) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < ol.children.length; i++) {
|
||||
const el = ol.children[i];
|
||||
const id = el.dataset.toastId;
|
||||
registerSwipe(id);
|
||||
refreshProperties();
|
||||
registerRemoveTimeout(el);
|
||||
}
|
||||
return ol;
|
||||
}
|
||||
|
||||
function registerMouseOver(ol) {
|
||||
ol.addEventListener("mouseenter", function () {
|
||||
ol.dataset.expanded = true;
|
||||
for(let el of ol.children) {
|
||||
clearRemoveTimeout(el);
|
||||
}
|
||||
});
|
||||
|
||||
ol.addEventListener("mouseleave", function () {
|
||||
ol.dataset.expanded = false;
|
||||
for(let el of ol.children) {
|
||||
registerRemoveTimeout(el);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerKeyboardShortcuts(ol) {
|
||||
window.addEventListener("keydown", function (e) {
|
||||
if (e.altKey && e.code === "KeyT") {
|
||||
if (ol.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
ol.dataset.expanded = ol.dataset.expanded === "true" ? "false" : "true";;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearRemoveTimeout(el) {
|
||||
const tid = el.dataset.removeTid;
|
||||
if (tid) window.clearTimeout(tid);
|
||||
}
|
||||
|
||||
function refreshProperties() {
|
||||
const list = document.getElementById("toaster");
|
||||
let heightsBefore = 0;
|
||||
let removed = 0;
|
||||
for (let i = 0; i < list.children.length; i++) {
|
||||
const el = list.children[i];
|
||||
if (el.dataset.removed === "true") {
|
||||
removed++;
|
||||
continue;
|
||||
}
|
||||
const idx = i - removed;
|
||||
|
||||
el.dataset.index = idx;
|
||||
el.dataset.front = (idx === 0).toString();
|
||||
el.dataset.hidden = (idx > VISIBLE_TOASTS_AMOUNT).toString();
|
||||
el.style.setProperty("--index", idx);
|
||||
el.style.setProperty("--offset", `${GAP * idx + heightsBefore}px`);
|
||||
el.style.setProperty("z-index", list.children.length - i);
|
||||
heightsBefore += Number(el.dataset.initialHeight);
|
||||
}
|
||||
list.style.setProperty('--full-height', `${heightsBefore + GAP * list.children.length}px`);
|
||||
}
|
||||
6
resources/js/notification/helpers.js
Normal file
6
resources/js/notification/helpers.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export function genId() {
|
||||
return (
|
||||
Date.now().toString(36) +
|
||||
Math.random().toString(36).substring(2, 12).padStart(12, 0)
|
||||
);
|
||||
}
|
||||
41
resources/js/notification/icons.js
Normal file
41
resources/js/notification/icons.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const SuccessIcon = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
|
||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
|
||||
</svg>
|
||||
<span class="sr-only">Check icon</span>
|
||||
</div>`;
|
||||
|
||||
const WarningIcon = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-orange-500 bg-orange-100 rounded-lg dark:bg-orange-700 dark:text-orange-200">
|
||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/>
|
||||
</svg>
|
||||
<span class="sr-only">Warning icon</span>
|
||||
</div>`;
|
||||
|
||||
const InfoIcon = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-blue-500 bg-blue-100 rounded-lg dark:bg-blue-800 dark:text-blue-200">
|
||||
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm9.408-5.5a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2h-.01ZM10 10a1 1 0 1 0 0 2h1v3h-1a1 1 0 1 0 0 2h4a1 1 0 1 0 0-2h-1v-4a1 1 0 0 0-1-1h-2Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="sr-only">Error icon</span>
|
||||
</div>`;
|
||||
|
||||
const ErrorIcon = `
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
|
||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
|
||||
</svg>
|
||||
<span class="sr-only">Error icon</span>
|
||||
</div>`;
|
||||
|
||||
export default function getIcon(type) {
|
||||
switch (type) {
|
||||
case "success": return SuccessIcon;
|
||||
case "info": return InfoIcon;
|
||||
case "warning": return WarningIcon;
|
||||
case "error": return ErrorIcon;
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
57
resources/js/notification/touch.js
Normal file
57
resources/js/notification/touch.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const SWIPE_THRESHOLD = 50;
|
||||
|
||||
export default function registerSwipe(id) {
|
||||
const el = document.querySelector(`[data-toast-id="${id}"]`);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
let dragStartTime = null;
|
||||
let pointerStart = null;
|
||||
|
||||
el.addEventListener("pointerdown", function (event) {
|
||||
dragStartTime = new Date();
|
||||
event.target.setPointerCapture(event.pointerId);
|
||||
if (event.target.tagName === "BUTTON") {
|
||||
return;
|
||||
}
|
||||
el.dataset.swiping = true;
|
||||
pointerStart = { x: event.clientX, y: event.clientY };
|
||||
});
|
||||
|
||||
el.addEventListener("pointerup", function (event) {
|
||||
pointerStart = null;
|
||||
const swipeAmount = Number(
|
||||
el.style.getPropertyValue("--swipe-amount").replace("px", "") || 0,
|
||||
);
|
||||
const timeTaken = new Date().getTime() - dragStartTime.getTime();
|
||||
const velocity = Math.abs(swipeAmount) / timeTaken;
|
||||
|
||||
// Remove only if threshold is met
|
||||
if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
|
||||
ToastinTakin.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.setProperty("--swipe-amount", "0px");
|
||||
el.dataset.swiping = false;
|
||||
});
|
||||
|
||||
el.addEventListener("pointermove", function (event) {
|
||||
if (!pointerStart) {
|
||||
return;
|
||||
}
|
||||
const yPosition = event.clientY - pointerStart.y;
|
||||
const xPosition = event.clientX - pointerStart.x;
|
||||
|
||||
const clampedX = Math.max(0, xPosition);
|
||||
const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
|
||||
const isAllowedToSwipe = Math.abs(clampedX) > swipeStartThreshold;
|
||||
|
||||
if (isAllowedToSwipe) {
|
||||
el.style.setProperty("--swipe-amount", `${xPosition}px`);
|
||||
} else if (Math.abs(yPosition) > swipeStartThreshold) {
|
||||
// Swipe is heading into the wrong direction, possibly the user wants to abort.
|
||||
pointerStart = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
55
resources/js/notification/ui.js
Normal file
55
resources/js/notification/ui.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import getIcon from './icons';
|
||||
import { genId } from './helpers';
|
||||
|
||||
export function renderToaster() {
|
||||
const toaster = document.createElement("div");
|
||||
document.body.appendChild(toaster);
|
||||
toaster.outerHTML = `
|
||||
<div
|
||||
aria-label="Notifications alt+T"
|
||||
tabindex="-1"
|
||||
data-expanded="false"
|
||||
class="fixed right-4 top-4 z-50 w-80"
|
||||
id="toaster"
|
||||
></div>`;
|
||||
return toaster;
|
||||
}
|
||||
|
||||
export function renderToast(list, msg, type) {
|
||||
const toast = document.createElement("div");
|
||||
const id = genId();
|
||||
const count = list.children.length;
|
||||
const icon = getIcon(type);
|
||||
|
||||
list.prepend(toast);
|
||||
|
||||
toast.outerHTML = `
|
||||
<div
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
role="alert"
|
||||
class="absolute select-none transition-opacity transition-transform flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"
|
||||
tabindex="0"
|
||||
data-toast-id="${id}"
|
||||
data-removed="false"
|
||||
data-hidden="true"
|
||||
data-index="${0}"
|
||||
data-front="true"
|
||||
data-swiping="false"
|
||||
data-swipe-out="false"
|
||||
style="--index: 0; --offset: 0px; --initial-height: 0px;"
|
||||
>
|
||||
${ icon ? icon : ""}
|
||||
<div class="ms-3 text-sm font-normal">
|
||||
${msg}
|
||||
</div>
|
||||
<button onclick="ToastinTakin.remove('${id}')" type="button" class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#toast-success" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
return {id, toast};
|
||||
}
|
||||
626
resources/js/sonner.js
Normal file
626
resources/js/sonner.js
Normal file
@@ -0,0 +1,626 @@
|
||||
////////////////////////
|
||||
// Sonner
|
||||
////////////////////////
|
||||
|
||||
////////////////////////
|
||||
// Constants
|
||||
////////////////////////
|
||||
const VISIBLE_TOASTS_AMOUNT = 4;
|
||||
const VIEWPORT_OFFSET = "32px";
|
||||
const TOAST_LIFETIME = 4000;
|
||||
const TOAST_WIDTH = 356;
|
||||
const GAP = 14;
|
||||
const SWIPE_THRESHOLD = 20;
|
||||
const TIME_BEFORE_UNMOUNT = 200;
|
||||
|
||||
////////////////////////
|
||||
// Sonner
|
||||
// The Sonner object is a singleton that provides methods to show different types of toasts.
|
||||
////////////////////////
|
||||
window.Sonner = {
|
||||
/**
|
||||
* Initializes the toasters in the DOM.
|
||||
* The function creates a new section element and an ordered list element inside it.
|
||||
* @param {Object} options - An object with the following properties:
|
||||
* @param {boolean} options.closeButton - A boolean to control the visibility of the close button on the toasts.
|
||||
* @param {boolean} options.richColors - A boolean to control the use of rich colors for the toasts.
|
||||
* @param {string} options.position - A string to control the position of the toasts. The string is a combination of two values: the vertical position (top or bottom) and the horizontal position (left or right).
|
||||
* @returns {void}
|
||||
* @example
|
||||
* Sonner.init({ closeButton: true, richColors: true, position: "bottom-right" });
|
||||
*/
|
||||
init({
|
||||
closeButton = false,
|
||||
richColors = false,
|
||||
position = "bottom-right",
|
||||
} = {}) {
|
||||
if (reinitializeToaster()) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderToaster({ closeButton, richColors, position });
|
||||
// loadSonnerStyles();
|
||||
|
||||
const ol = document.getElementById("sonner-toaster-list");
|
||||
registerMouseOver(ol);
|
||||
registerKeyboardShortcuts(ol);
|
||||
},
|
||||
/**
|
||||
* Shows a new success toast with a specific message.
|
||||
* @param {string} msg - The message to display in the toast.
|
||||
* @returns {void}
|
||||
*/
|
||||
success(msg, opts = {}) {
|
||||
return Sonner.show(msg, { icon: 'success', type: "success", ...opts });
|
||||
},
|
||||
/**
|
||||
* Shows a new error toast with a specific message.
|
||||
* @param {string} msg - The message to display in the toast.
|
||||
* @returns {void}
|
||||
*/
|
||||
error(msg, opts = {}) {
|
||||
return Sonner.show(msg, { icon: 'error', type: "error", ...opts });
|
||||
},
|
||||
/**
|
||||
* Shows a new info toast with a specific message.
|
||||
* @param {string} msg - The message to display in the toast.
|
||||
* @returns {void}
|
||||
*/
|
||||
info(msg, opts = {}) {
|
||||
return Sonner.show(msg, { icon: 'info', type: "info", ...opts });
|
||||
},
|
||||
/**
|
||||
* Shows a new warning toast with a specific message.
|
||||
* @param {string} msg - The message to display in the toast.
|
||||
* @returns {void}
|
||||
*/
|
||||
warning(msg, opts = {}) {
|
||||
return Sonner.show(msg, { icon: 'warning', type: "warning", ...opts });
|
||||
},
|
||||
/**
|
||||
* Shows a promise loading toast
|
||||
* @template T promise data type
|
||||
* @param {Promise<T>} promise
|
||||
* @param {Object} opts options
|
||||
* @param {string} opts.loading message to display while loading
|
||||
* @param {string|(data : T) => string} opts.success function callback / message to show when loaded
|
||||
* @param {string|(data : Error) => string} opts.success function callback / message to show when errored
|
||||
*/
|
||||
promise(promise, opts = {}) {
|
||||
const toast = Sonner.show(opts.loading ?? 'Loading...', {
|
||||
icon: 'loading',
|
||||
type: 'loading',
|
||||
...opts,
|
||||
duration: -1,
|
||||
});
|
||||
|
||||
promise
|
||||
.then(result => {
|
||||
// Update the message and start the timeout
|
||||
const msg = typeof opts.success === 'string' ? opts.success : opts.success(result);
|
||||
toast.setTitle(msg).setIcon('success').setDuration(opts.duration ?? TOAST_LIFETIME);
|
||||
return result;
|
||||
})
|
||||
.catch(err => {
|
||||
const msg = typeof opts.error === 'string' ? opts.error : opts.error(err);
|
||||
toast.setTitle(msg).setIcon('error').setDuration(opts.duration ?? TOAST_LIFETIME);
|
||||
throw err;
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a new toast with a specific message, description, and type.
|
||||
* @param {string} msg - The message to display in the toast.
|
||||
* @param {Object} options - An object with the following properties:
|
||||
* @param {string} options.type - The type of the toast. The type can be one of the following values: "success", "error", "info", "warning", or "neutral".
|
||||
* @param {string} options.description - The description to display in the toast.
|
||||
* @returns {void}
|
||||
*/
|
||||
show(msg, opts = {}) {
|
||||
const list = document.getElementById("sonner-toaster-list");
|
||||
const { toast, id } = renderToast(list, msg, opts);
|
||||
|
||||
// Wait for the toast to be mounted before registering swipe events
|
||||
//window.setTimeout(function () {
|
||||
const el = list.children[0];
|
||||
const height = el.getBoundingClientRect().height;
|
||||
|
||||
el.setAttribute("data-mounted", "true");
|
||||
el.setAttribute("data-initial-height", height);
|
||||
el.style.setProperty("--initial-height", `${height}px`);
|
||||
list.style.setProperty("--front-toast-height", `${height}px`);
|
||||
|
||||
registerSwipe(id);
|
||||
refreshProperties();
|
||||
toast.setDuration(opts.duration ?? TOAST_LIFETIME);
|
||||
//}, 16);
|
||||
return toast;
|
||||
},
|
||||
/**
|
||||
* Removes an element with a specific id from the DOM after a delay.
|
||||
* The element is marked as removed and any previous unmount timeout is cleared.
|
||||
* A new timeout is set to remove the element from its parent.
|
||||
* The timeout ensures that all CSS transitions complete before the element is removed.
|
||||
*
|
||||
* @param {string} id - The data-id attribute of the element to remove.
|
||||
*/
|
||||
remove(id) {
|
||||
const el = document.querySelector(`[data-id="${id}"]`);
|
||||
if (!el) return;
|
||||
|
||||
el.setAttribute("data-removed", "true");
|
||||
refreshProperties();
|
||||
|
||||
const previousTid = el.getAttribute("data-unmount-tid");
|
||||
if (previousTid) window.clearTimeout(previousTid);
|
||||
|
||||
const tid = window.setTimeout(function () {
|
||||
el.parentElement?.removeChild(el);
|
||||
}, TIME_BEFORE_UNMOUNT);
|
||||
el.setAttribute("data-unmount-tid", tid);
|
||||
},
|
||||
};
|
||||
|
||||
////////////////////////
|
||||
// Assets
|
||||
////////////////////////
|
||||
|
||||
const getIcon = (type) => {
|
||||
switch (type) {
|
||||
case "success":
|
||||
return SuccessIcon;
|
||||
|
||||
case "info":
|
||||
return InfoIcon;
|
||||
|
||||
case "warning":
|
||||
return WarningIcon;
|
||||
|
||||
case "error":
|
||||
return ErrorIcon;
|
||||
|
||||
case 'loading':
|
||||
return Loader;
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const bars = Array(12).fill(0);
|
||||
|
||||
const Loader = `
|
||||
<div class="sonner-loading-wrapper" data-visible='${true}'>
|
||||
<div class="sonner-spinner">
|
||||
${bars.map((_, i) => `<div class="sonner-loading-bar" key="spinner-bar-${i}"></div>`).join('\n')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
const SuccessIcon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="20" width="20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>`;
|
||||
|
||||
const WarningIcon = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>`;
|
||||
|
||||
const InfoIcon = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>`;
|
||||
|
||||
const ErrorIcon = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewbox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>`;
|
||||
|
||||
////////////////////////
|
||||
// Auxiliary functions
|
||||
////////////////////////
|
||||
|
||||
/**
|
||||
* Generates a unique id for a toast.
|
||||
* The function generates a unique id by combining the current timestamp with a random string.
|
||||
* The function returns the unique id as a string.
|
||||
* @returns {string} - The unique id.
|
||||
* @example
|
||||
* const id = genid();
|
||||
*/
|
||||
function genid() {
|
||||
return (
|
||||
Date.now().toString(36) +
|
||||
Math.random().toString(36).substring(2, 12).padStart(12, 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new toast element and returns it along with its id.
|
||||
* The function creates a new list item element and sets its outerHTML to a string containing the toast structure.
|
||||
* The function also generates a unique id for the toast and returns it along with the toast element.
|
||||
* @param {Element} list - The list element to append the toast to.
|
||||
* @param {string} msg - The message to display in the toast.
|
||||
* @param {Object} options - An object with the following properties:
|
||||
* @param {string} options.type - The type of the toast. The type can be one of the following values: "success", "error", "info", "warning", or "neutral".
|
||||
* @param {string} options.description - The description to display in the toast.
|
||||
* @returns {Object} - An object with the following properties:
|
||||
* @returns {Element} toast - The toast element.
|
||||
* @returns {string} id - The unique id of the toast.
|
||||
*/
|
||||
function renderToast(list, msg, opts = {}) {
|
||||
const toast = document.createElement("div");
|
||||
list.prepend(toast);
|
||||
const id = genid();
|
||||
const count = list.children.length;
|
||||
const asset = getIcon(opts.icon) ?? opts.icon;
|
||||
|
||||
|
||||
toast.outerHTML = `<li
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
role="status"
|
||||
tabindex="0"
|
||||
data-id="${id}"
|
||||
data-type="${opts.type}"
|
||||
data-sonner-toast=""
|
||||
data-mounted="false"
|
||||
data-styled="true"
|
||||
data-promise="false"
|
||||
data-removed="false"
|
||||
data-visible="true"
|
||||
data-y-position="${list.getAttribute("data-y-position")}"
|
||||
data-x-position="${list.getAttribute("data-x-position")}"
|
||||
data-index="${0}"
|
||||
data-front="true"
|
||||
data-swiping="false"
|
||||
data-dismissible="true"
|
||||
data-swipe-out="false"
|
||||
data-expanded="false"
|
||||
style="--index: 0; --toasts-before: ${0}; --z-index: ${count}; --offset: 0px; --initial-height: 0px;"
|
||||
>
|
||||
${list.getAttribute("data-close-button") === "true"
|
||||
? `<button
|
||||
aria-label="Close"
|
||||
data-disabled=""
|
||||
class="absolute top-0.5 right-0.5 border border-neutral-800 text-neutral-800 bg-neutral-100 rounded-sm"
|
||||
onclick="Sonner.remove('${id}')"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${asset
|
||||
? `<div data-icon="" class="">${asset}</div>`
|
||||
: `<div data-icon="" class=""></div>`
|
||||
}
|
||||
<div
|
||||
data-content=""
|
||||
class="">
|
||||
<div data-title="" class="">
|
||||
${msg}
|
||||
</div>
|
||||
${opts.description
|
||||
? `<div data-description="" class="">${opts.description}</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
|
||||
return {
|
||||
id,
|
||||
toast: {
|
||||
target: document.querySelector(`[data-id="${id}"]`),
|
||||
setTitle: function (msg, raw = false) {
|
||||
const title = document.querySelector(`[data-sonner-toast][data-id=${id}] [data-title]`);
|
||||
if (raw) title.innerHTML = msg;
|
||||
else title.textContent = msg;
|
||||
return this;
|
||||
},
|
||||
setIcon: function (icon) {
|
||||
const ico = getIcon(icon) ?? '';
|
||||
document.querySelector(`[data-sonner-toast][data-id=${id}] [data-icon]`).innerHTML = ico;
|
||||
return this;
|
||||
},
|
||||
setDuration: function(duration) {
|
||||
this.target.setAttribute('data-duration', duration);
|
||||
registerRemoveTimeout(this.target);
|
||||
return this;
|
||||
},
|
||||
dismiss: function() {
|
||||
Sonner.remove(this.target.getAttribute("data-id"))
|
||||
return this;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new remove timeout for a specific element.
|
||||
* The function sets a new timeout to remove the element from its parent after a delay.
|
||||
* The timeout ensures that all CSS transitions complete before the element is removed.
|
||||
* @param {Element} el - The element to register the remove timeout for.
|
||||
* @param {number} lifetime - How long the toast will last for
|
||||
* @returns {void}
|
||||
*/
|
||||
function registerRemoveTimeout(el) {
|
||||
if (!el.getAttribute("data-id"))
|
||||
throw new Error('invalid target for removal');
|
||||
|
||||
const lifetime = el.getAttribute('data-duration') ?? TOAST_LIFETIME;
|
||||
if (lifetime < 0)
|
||||
return;
|
||||
|
||||
// Clear previous duration
|
||||
if (el.getAttribute("data-remove-tid"))
|
||||
window.clearTimeout(el.getAttribute("data-remove-tid"));
|
||||
|
||||
// Set new timeout
|
||||
const tid = window.setTimeout(() => {
|
||||
Sonner.remove(el.getAttribute("data-id"));
|
||||
}, lifetime);
|
||||
el.setAttribute("data-remove-tid", tid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitializes the toaster and its children in the DOM.
|
||||
* @returns {Element} - The ordered list element with the sonner-toaster-list id.
|
||||
*/
|
||||
function reinitializeToaster() {
|
||||
const ol = document.getElementById("sonner-toaster-list");
|
||||
if (!ol) return;
|
||||
for (let i = 0; i < ol.children.length; i++) {
|
||||
const el = ol.children[i];
|
||||
const id = el.getAttribute("data-id");
|
||||
registerSwipe(id);
|
||||
refreshProperties();
|
||||
registerRemoveTimeout(el);
|
||||
}
|
||||
return ol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the toaster in the DOM.
|
||||
* @param {Object} options - An object with the following properties:
|
||||
* @param {boolean} options.closeButton - A boolean to control the visibility of the close button on the toasts.
|
||||
* @param {boolean} options.richColors - A boolean to control the use of rich colors for the toasts.
|
||||
* @param {string} options.position - A string to control the position of the toasts. The string is a combination of two values: the vertical position (top or bottom) and the horizontal position (left or right).
|
||||
* @returns {void}
|
||||
*/
|
||||
function renderToaster({ closeButton, richColors, position }) {
|
||||
const el = document.createElement("div");
|
||||
document.body.appendChild(el);
|
||||
position = position.split("-");
|
||||
el.outerHTML = `
|
||||
<section aria-label="Notifications alt+T" tabindex="-1">
|
||||
<ol
|
||||
dir="ltr"
|
||||
tabindex="-1"
|
||||
data-sonner-toaster="true"
|
||||
data-theme="light"
|
||||
data-close-button="${closeButton}"
|
||||
data-rich-colors="${richColors}"
|
||||
data-y-position="${position[0]}"
|
||||
data-x-position="${position[1]}"
|
||||
style="--front-toast-height: 0px; --offset: ${VIEWPORT_OFFSET}; --width: ${TOAST_WIDTH}px; --gap: ${GAP}px;"
|
||||
id="sonner-toaster-list"
|
||||
></ol>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the Sonner styles in the DOM.
|
||||
* @returns {void}
|
||||
*/
|
||||
function loadSonnerStyles() {
|
||||
var link = document.createElement("link");
|
||||
link.href = "./sonner.css";
|
||||
link.type = "text/css";
|
||||
link.rel = "stylesheet";
|
||||
link.media = "screen";
|
||||
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers mouse over events on a specific ordered list element in the DOM.
|
||||
* @param {Element} ol - The ordered list element to register mouse over events on.
|
||||
* @returns {void}
|
||||
*/
|
||||
function registerMouseOver(ol) {
|
||||
ol.addEventListener("mouseenter", function () {
|
||||
for (let i = 0; i < ol.children.length; i++) {
|
||||
const el = ol.children[i];
|
||||
if (el.getAttribute("data-expanded") === "true") continue;
|
||||
el.setAttribute("data-expanded", "true");
|
||||
|
||||
clearRemoveTimeout(el);
|
||||
}
|
||||
});
|
||||
ol.addEventListener("mouseleave", function () {
|
||||
for (let i = 0; i < ol.children.length; i++) {
|
||||
const el = ol.children[i];
|
||||
if (el.getAttribute("data-expanded") === "false") continue;
|
||||
el.setAttribute("data-expanded", "false");
|
||||
|
||||
registerRemoveTimeout(el);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers keyboard shortcuts for the ordered list element in the DOM.
|
||||
* The function listens for the Alt+T key combination to expand or collapse the toasts.
|
||||
* @param {Element} ol - The ordered list element to register keyboard shortcuts for.
|
||||
* @returns {void}
|
||||
*/
|
||||
function registerKeyboardShortcuts(ol) {
|
||||
window.addEventListener("keydown", function (e) {
|
||||
if (e.altKey && e.code === "KeyT") {
|
||||
if (ol.children.length === 0) return;
|
||||
const expanded = ol.children[0].getAttribute("data-expanded");
|
||||
const newExpanded = expanded === "true" ? "false" : "true";
|
||||
for (let i = 0; i < ol.children.length; i++) {
|
||||
ol.children[i].setAttribute("data-expanded", newExpanded);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the remove timeout for a specific element.
|
||||
* @param {Element} el - The element to clear the remove timeout for.
|
||||
* @returns {void}
|
||||
*/
|
||||
function clearRemoveTimeout(el) {
|
||||
const tid = el.getAttribute("data-remove-tid");
|
||||
if (tid) window.clearTimeout(tid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the properties of the children of a specific list element in the DOM.
|
||||
* The function iterates over each child of the list, skipping those marked as removed.
|
||||
* For each remaining child, it updates various data attributes and CSS properties related to its index, visibility, offset, and z-index.
|
||||
* The function also keeps track of the cumulative height of the elements processed so far to calculate the offset for each element.
|
||||
*/
|
||||
function refreshProperties() {
|
||||
const list = document.getElementById("sonner-toaster-list");
|
||||
let heightsBefore = 0;
|
||||
let removed = 0;
|
||||
for (let i = 0; i < list.children.length; i++) {
|
||||
const el = list.children[i];
|
||||
if (el.getAttribute("data-removed") === "true") {
|
||||
removed++;
|
||||
continue;
|
||||
}
|
||||
const idx = i - removed;
|
||||
el.setAttribute("data-index", idx);
|
||||
el.setAttribute("data-front", idx === 0 ? "true" : "false");
|
||||
el.setAttribute(
|
||||
"data-visible",
|
||||
idx < VISIBLE_TOASTS_AMOUNT ? "true" : "false",
|
||||
);
|
||||
el.style.setProperty("--index", idx);
|
||||
el.style.setProperty("--toasts-before", idx);
|
||||
el.style.setProperty("--offset", `${GAP * idx + heightsBefore}px`);
|
||||
el.style.setProperty("--z-index", list.children.length - i);
|
||||
heightsBefore += Number(el.getAttribute("data-initial-height"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers swipe events on an element with a specific id.
|
||||
* The element is selected using the id and event listeners are added for pointerdown, pointerup, and pointermove events.
|
||||
* The swipe gesture is calculated based on the movement of the pointer and the time taken for the swipe.
|
||||
* If the swipe meets a certain threshold or velocity, the element is marked for removal.
|
||||
* If the swipe does not meet the threshold, the swipe is reset.
|
||||
* For more information on the swipe gesture, see the following article:
|
||||
* https://emilkowal.ski/ui/building-a-toast-component
|
||||
*
|
||||
* @param {string} id - The data-id attribute of the element to register swipe events on.
|
||||
*/
|
||||
function registerSwipe(id) {
|
||||
const el = document.querySelector(`[data-id="${id}"]`);
|
||||
if (!el) return;
|
||||
let dragStartTime = null;
|
||||
let pointerStart = null;
|
||||
const y = el.getAttribute("data-y-position");
|
||||
el.addEventListener("pointerdown", function (event) {
|
||||
dragStartTime = new Date();
|
||||
event.target.setPointerCapture(event.pointerId);
|
||||
if (event.target.tagName === "BUTTON") return;
|
||||
el.setAttribute("data-swiping", "true");
|
||||
pointerStart = { x: event.clientX, y: event.clientY };
|
||||
});
|
||||
el.addEventListener("pointerup", function (event) {
|
||||
pointerStart = null;
|
||||
const swipeAmount = Number(
|
||||
el.style.getPropertyValue("--swipe-amount").replace("px", "") || 0,
|
||||
);
|
||||
const timeTaken = new Date().getTime() - dragStartTime.getTime();
|
||||
const velocity = Math.abs(swipeAmount) / timeTaken;
|
||||
|
||||
// Remove only if threshold is met
|
||||
if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
|
||||
el.setAttribute("data-swipe-out", "true");
|
||||
Sonner.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.setProperty("--swipe-amount", "0px");
|
||||
el.setAttribute("data-swiping", "false");
|
||||
});
|
||||
|
||||
el.addEventListener("pointermove", function (event) {
|
||||
if (!pointerStart) return;
|
||||
const yPosition = event.clientY - pointerStart.y;
|
||||
const xPosition = event.clientX - pointerStart.x;
|
||||
|
||||
const clamp = y === "top" ? Math.min : Math.max;
|
||||
const clampedY = clamp(0, yPosition);
|
||||
const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
|
||||
const isAllowedToSwipe = Math.abs(clampedY) > swipeStartThreshold;
|
||||
|
||||
if (isAllowedToSwipe) {
|
||||
el.style.setProperty("--swipe-amount", `${yPosition}px`);
|
||||
} else if (Math.abs(xPosition) > swipeStartThreshold) {
|
||||
// User is swiping in wrong direction so we disable swipe gesture
|
||||
// for the current pointer down interaction
|
||||
pointerStart = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user