fix(02-04): improve swipe-to-delete with direct touch handlers
- Replace svelte-gestures with native touch event handlers - Add invalidateAll() after save/delete for seamless list updates - Add $effect to sync edit state when entry prop changes - Improve swipe animation with isSwiping state tracking
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Entry } from '$lib/server/db/schema';
|
import type { Entry } from '$lib/server/db/schema';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
import { invalidateAll } from '$app/navigation';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
entry: Entry;
|
entry: Entry;
|
||||||
@@ -13,11 +13,20 @@
|
|||||||
// Expand/collapse state
|
// Expand/collapse state
|
||||||
let expanded = $state(false);
|
let expanded = $state(false);
|
||||||
|
|
||||||
// Edit state - initialize from entry
|
// Edit state - use $derived to stay in sync with entry prop
|
||||||
let editTitle = $state(entry.title || '');
|
let editTitle = $state(entry.title || '');
|
||||||
let editContent = $state(entry.content);
|
let editContent = $state(entry.content);
|
||||||
let editType = $state(entry.type);
|
let editType = $state(entry.type);
|
||||||
|
|
||||||
|
// Sync edit state when entry changes (after invalidateAll)
|
||||||
|
$effect(() => {
|
||||||
|
if (!expanded) {
|
||||||
|
editTitle = entry.title || '';
|
||||||
|
editContent = entry.content;
|
||||||
|
editType = entry.type;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Debounced auto-save
|
// Debounced auto-save
|
||||||
let saveTimeout: ReturnType<typeof setTimeout>;
|
let saveTimeout: ReturnType<typeof setTimeout>;
|
||||||
let isSaving = $state(false);
|
let isSaving = $state(false);
|
||||||
@@ -25,20 +34,34 @@
|
|||||||
// Swipe state
|
// Swipe state
|
||||||
let swipeOffset = $state(0);
|
let swipeOffset = $state(0);
|
||||||
let isConfirmingDelete = $state(false);
|
let isConfirmingDelete = $state(false);
|
||||||
|
let touchStartX = 0;
|
||||||
|
let isSwiping = $state(false);
|
||||||
|
|
||||||
function handleSwipe(event: SwipeCustomEvent) {
|
function handleTouchStart(e: TouchEvent) {
|
||||||
const { direction } = event.detail;
|
touchStartX = e.touches[0].clientX;
|
||||||
if (direction === 'left') {
|
isSwiping = true;
|
||||||
isConfirmingDelete = true;
|
}
|
||||||
|
|
||||||
|
function handleTouchMove(e: TouchEvent) {
|
||||||
|
if (!isSwiping) return;
|
||||||
|
const currentX = e.touches[0].clientX;
|
||||||
|
const diff = currentX - touchStartX;
|
||||||
|
// Only allow left swipe (negative diff), cap at -100px
|
||||||
|
if (diff < 0) {
|
||||||
|
swipeOffset = Math.max(diff, -100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create swipe gesture using useSwipe hook
|
function handleTouchEnd() {
|
||||||
const swipeGesture = useSwipe(handleSwipe, () => ({
|
isSwiping = false;
|
||||||
timeframe: 300,
|
// If swiped far enough, show confirmation
|
||||||
minSwipeDistance: 60,
|
if (swipeOffset < -60) {
|
||||||
touchAction: 'pan-y'
|
isConfirmingDelete = true;
|
||||||
}));
|
swipeOffset = -100; // Snap to reveal delete area
|
||||||
|
} else {
|
||||||
|
swipeOffset = 0; // Snap back
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function cancelDelete() {
|
function cancelDelete() {
|
||||||
isConfirmingDelete = false;
|
isConfirmingDelete = false;
|
||||||
@@ -54,7 +77,7 @@
|
|||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
window.location.reload();
|
await invalidateAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function debouncedSave() {
|
async function debouncedSave() {
|
||||||
@@ -72,6 +95,8 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
// Refresh data so list shows updated values
|
||||||
|
await invalidateAll();
|
||||||
} finally {
|
} finally {
|
||||||
isSaving = false;
|
isSaving = false;
|
||||||
}
|
}
|
||||||
@@ -118,10 +143,12 @@
|
|||||||
|
|
||||||
<!-- Swipeable entry card -->
|
<!-- Swipeable entry card -->
|
||||||
<article
|
<article
|
||||||
{...swipeGesture}
|
ontouchstart={handleTouchStart}
|
||||||
style="transform: translateX({swipeOffset}px); transition: {swipeOffset === 0
|
ontouchmove={handleTouchMove}
|
||||||
? 'transform 0.2s'
|
ontouchend={handleTouchEnd}
|
||||||
: 'none'}"
|
style="transform: translateX({swipeOffset}px); transition: {isSwiping
|
||||||
|
? 'none'
|
||||||
|
: 'transform 0.2s'}"
|
||||||
class="p-4 border-b border-gray-100 md:border md:rounded-lg md:shadow-sm md:mb-3 bg-white relative"
|
class="p-4 border-b border-gray-100 md:border md:rounded-lg md:shadow-sm md:mb-3 bg-white relative"
|
||||||
>
|
>
|
||||||
<!-- Collapsed view - clickable header -->
|
<!-- Collapsed view - clickable header -->
|
||||||
|
|||||||
Reference in New Issue
Block a user