mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-13 01:02:25 -06:00
refac
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
const UNSAFE_EDGE = 16;
|
||||
const SWIPE_SLOP = 8;
|
||||
const AXIS_RATIO = 1.2;
|
||||
const FLICK_VELOCITY = 0.55;
|
||||
const SETTLE_MS = 210;
|
||||
|
||||
export let open = false;
|
||||
export let enabled = false;
|
||||
export let width = 245;
|
||||
export let onOpenChange: (open: boolean) => void = () => {};
|
||||
|
||||
let mounted = false;
|
||||
let active = false;
|
||||
let locked = false;
|
||||
let direction: 'open' | 'close' | null = null;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let lastX = 0;
|
||||
let lastTime = 0;
|
||||
let velocity = 0;
|
||||
let startProgress = 0;
|
||||
let swipeProgress = 0;
|
||||
let settling = false;
|
||||
let settleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let settleFrame: ReturnType<typeof requestAnimationFrame> | null = null;
|
||||
let scrollLocked = false;
|
||||
let lastOpen: boolean | null = null;
|
||||
let lastEnabled: boolean | null = null;
|
||||
|
||||
$: panelWidth = width || 245;
|
||||
$: progress = enabled ? Math.max(0, Math.min(1, swipeProgress)) : 1;
|
||||
$: visible = open || (enabled && mounted);
|
||||
$: panelStyle = enabled
|
||||
? `transform: translateX(${(progress - 1) * 100}%); transition: ${
|
||||
settling ? `transform ${SETTLE_MS}ms cubic-bezier(0.22, 1, 0.36, 1)` : 'none'
|
||||
};`
|
||||
: '';
|
||||
$: backdropStyle = enabled
|
||||
? `opacity: ${progress * 0.6}; transition: ${
|
||||
settling ? `opacity ${SETTLE_MS}ms cubic-bezier(0.22, 1, 0.36, 1)` : 'none'
|
||||
};`
|
||||
: '';
|
||||
|
||||
$: if (open !== lastOpen || enabled !== lastEnabled) {
|
||||
lastOpen = open;
|
||||
lastEnabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
animate(open);
|
||||
} else {
|
||||
mounted = false;
|
||||
swipeProgress = open ? 1 : 0;
|
||||
settling = false;
|
||||
setScrollLock(false);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldSkipSwipe = (target: EventTarget | null) => {
|
||||
return (
|
||||
target instanceof Element &&
|
||||
!!target.closest(
|
||||
'input, textarea, select, button, [role="menu"], [contenteditable="true"], [data-sidebar-no-gesture]'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const setScrollLock = (locked: boolean) => {
|
||||
if (scrollLocked === locked || typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollLocked = locked;
|
||||
document.documentElement.classList.toggle('mobile-swipe-panel-active', locked);
|
||||
document.body.classList.toggle('mobile-swipe-panel-active', locked);
|
||||
};
|
||||
|
||||
const clearTimer = () => {
|
||||
if (settleTimer) {
|
||||
clearTimeout(settleTimer);
|
||||
settleTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const clearFrame = () => {
|
||||
if (settleFrame) {
|
||||
cancelAnimationFrame(settleFrame);
|
||||
settleFrame = null;
|
||||
}
|
||||
};
|
||||
|
||||
const prefersReducedMotion = () =>
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
const animate = (nextOpen: boolean) => {
|
||||
clearTimer();
|
||||
clearFrame();
|
||||
mounted = nextOpen || mounted;
|
||||
|
||||
const finish = () => {
|
||||
settling = false;
|
||||
mounted = nextOpen;
|
||||
settleTimer = null;
|
||||
};
|
||||
|
||||
const settle = () => {
|
||||
settling = !prefersReducedMotion();
|
||||
swipeProgress = nextOpen ? 1 : 0;
|
||||
settleTimer = setTimeout(finish, settling ? SETTLE_MS : 0);
|
||||
};
|
||||
|
||||
if (nextOpen && swipeProgress <= 0.01) {
|
||||
swipeProgress = 0;
|
||||
settling = false;
|
||||
settleFrame = requestAnimationFrame(() => {
|
||||
settleFrame = null;
|
||||
settle();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
settle();
|
||||
};
|
||||
|
||||
const resetSwipe = () => {
|
||||
active = false;
|
||||
locked = false;
|
||||
direction = null;
|
||||
velocity = 0;
|
||||
setScrollLock(false);
|
||||
};
|
||||
|
||||
const cancelSwipe = () => {
|
||||
resetSwipe();
|
||||
animate(open);
|
||||
};
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
if (!enabled || e.touches.length !== 1 || shouldSkipSwipe(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = e.touches[0];
|
||||
const x = touch.clientX;
|
||||
|
||||
if (!open) {
|
||||
if (x < UNSAFE_EDGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
direction = 'open';
|
||||
startProgress = 0;
|
||||
mounted = true;
|
||||
swipeProgress = 0;
|
||||
} else {
|
||||
direction = 'close';
|
||||
startProgress = swipeProgress || 1;
|
||||
}
|
||||
|
||||
clearTimer();
|
||||
settling = false;
|
||||
active = true;
|
||||
locked = false;
|
||||
startX = x;
|
||||
startY = touch.clientY;
|
||||
lastX = x;
|
||||
lastTime = e.timeStamp;
|
||||
velocity = 0;
|
||||
};
|
||||
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
if (!active || !direction || e.touches.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = e.touches[0];
|
||||
const dx = touch.clientX - startX;
|
||||
const dy = touch.clientY - startY;
|
||||
const absX = Math.abs(dx);
|
||||
const absY = Math.abs(dy);
|
||||
|
||||
if (!locked) {
|
||||
if ((direction === 'open' && dx > 0) || (direction === 'close' && dx < 0)) {
|
||||
swipeProgress = Math.max(0, Math.min(1, startProgress + dx / panelWidth));
|
||||
}
|
||||
|
||||
if (absX < SWIPE_SLOP && absY < SWIPE_SLOP) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (absY > absX || absX < absY * AXIS_RATIO) {
|
||||
cancelSwipe();
|
||||
return;
|
||||
}
|
||||
|
||||
locked = true;
|
||||
setScrollLock(true);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const now = e.timeStamp;
|
||||
const elapsed = Math.max(1, now - lastTime);
|
||||
velocity = (touch.clientX - lastX) / elapsed;
|
||||
lastX = touch.clientX;
|
||||
lastTime = now;
|
||||
|
||||
swipeProgress = Math.max(0, Math.min(1, startProgress + dx / panelWidth));
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!locked) {
|
||||
cancelSwipe();
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldOpen = direction === 'open' ? swipeProgress > 0.35 : swipeProgress > 0.65;
|
||||
|
||||
if (velocity > FLICK_VELOCITY) {
|
||||
shouldOpen = true;
|
||||
} else if (velocity < -FLICK_VELOCITY) {
|
||||
shouldOpen = false;
|
||||
}
|
||||
|
||||
resetSwipe();
|
||||
animate(shouldOpen);
|
||||
onOpenChange(shouldOpen);
|
||||
};
|
||||
|
||||
const onTouchCancel = () => {
|
||||
if (active) {
|
||||
cancelSwipe();
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('touchstart', onTouchStart, { passive: true });
|
||||
window.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
window.addEventListener('touchend', onTouchEnd);
|
||||
window.addEventListener('touchcancel', onTouchCancel);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('touchstart', onTouchStart);
|
||||
window.removeEventListener('touchmove', onTouchMove);
|
||||
window.removeEventListener('touchend', onTouchEnd);
|
||||
window.removeEventListener('touchcancel', onTouchCancel);
|
||||
clearTimer();
|
||||
clearFrame();
|
||||
setScrollLock(false);
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot {visible} {progress} {panelStyle} {backdropStyle} />
|
||||
|
||||
<style>
|
||||
:global(html),
|
||||
:global(body) {
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
|
||||
:global(html.mobile-swipe-panel-active),
|
||||
:global(body.mobile-swipe-panel-active) {
|
||||
touch-action: pan-y;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,6 +58,8 @@
|
||||
import { createMessagesList } from '$lib/utils';
|
||||
import { getOutputText } from '$lib/components/chat/Messages/structuredOutput';
|
||||
|
||||
const MOBILE_SIDEBAR_SETTLE_MS = 210;
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -111,6 +113,12 @@
|
||||
}
|
||||
};
|
||||
|
||||
const focusChatInputSoon = () => {
|
||||
setTimeout(() => {
|
||||
document.getElementById('chat-input')?.focus({ preventScroll: true });
|
||||
}, MOBILE_SIDEBAR_SETTLE_MS);
|
||||
};
|
||||
|
||||
$: if (openPreview && closeActiveHoverPreview !== closeHoverPreview) {
|
||||
closeActiveHoverPreview?.();
|
||||
closeActiveHoverPreview = closeHoverPreview;
|
||||
@@ -569,6 +577,7 @@
|
||||
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
focusChatInputSoon();
|
||||
}
|
||||
|
||||
// Optimistically mark as read in UI when clicked
|
||||
|
||||
Reference in New Issue
Block a user