mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-19 23:37:15 +08:00
feat(mobile): swipe to open menu for explorer (#8953)
close AF-1803 - bump theme - extract `SwipeHelper` and add `speed`, `direction` detection support - new mobile `SwipeMenu` component - integrate `SwipeMenu` to open a menu in Explorer - New `Haptics` module for mobile, implemented in `ios` and `mobile`(`navigator.vibrate()`) 
This commit is contained in:
@@ -233,7 +233,7 @@ export const useExplorerDocNodeOperationsMenu = (
|
||||
),
|
||||
},
|
||||
{
|
||||
index: 99,
|
||||
index: 97,
|
||||
view: (
|
||||
<MenuItem
|
||||
prefixIcon={<LinkedPageIcon />}
|
||||
@@ -244,7 +244,7 @@ export const useExplorerDocNodeOperationsMenu = (
|
||||
),
|
||||
},
|
||||
{
|
||||
index: 99,
|
||||
index: 98,
|
||||
view: (
|
||||
<MenuItem prefixIcon={<DuplicateIcon />} onClick={handleDuplicate}>
|
||||
{t['com.affine.header.option.duplicate']()}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { BaseExplorerTreeNodeProps } from '@affine/core/modules/explorer';
|
||||
import { ExplorerTreeContext } from '@affine/core/modules/explorer';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import { extractEmojiIcon } from '@affine/core/utils';
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { ArrowDownSmallIcon, MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import {
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { SwipeMenu } from '../../swipe-menu';
|
||||
import * as styles from './node.css';
|
||||
|
||||
interface ExplorerTreeNodeProps extends BaseExplorerTreeNodeProps {}
|
||||
@@ -43,6 +44,7 @@ export const ExplorerTreeNode = ({
|
||||
const clickForCollapse = !onClick && !to && !disabled;
|
||||
const [childCount, setChildCount] = useState(0);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const { emoji, name } = useMemo(() => {
|
||||
if (!extractEmojiAsIcon || !rawName) {
|
||||
@@ -160,15 +162,32 @@ export const ExplorerTreeNode = ({
|
||||
ref={rootRef}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.contentContainer} data-open={!collapsed}>
|
||||
{to ? (
|
||||
<LinkComponent to={to} className={styles.linkItemRoot}>
|
||||
{content}
|
||||
</LinkComponent>
|
||||
) : (
|
||||
<div>{content}</div>
|
||||
)}
|
||||
</div>
|
||||
<SwipeMenu
|
||||
onExecute={useCallback(() => setMenuOpen(true), [])}
|
||||
menu={
|
||||
<MobileMenu
|
||||
rootOptions={useMemo(
|
||||
() => ({ open: menuOpen, onOpenChange: setMenuOpen }),
|
||||
[menuOpen]
|
||||
)}
|
||||
items={menuOperations.map(({ view, index }) => (
|
||||
<Fragment key={index}>{view}</Fragment>
|
||||
))}
|
||||
>
|
||||
<MoreHorizontalIcon fontSize={24} />
|
||||
</MobileMenu>
|
||||
}
|
||||
>
|
||||
<div className={styles.contentContainer} data-open={!collapsed}>
|
||||
{to ? (
|
||||
<LinkComponent to={to} className={styles.linkItemRoot}>
|
||||
{content}
|
||||
</LinkComponent>
|
||||
) : (
|
||||
<div>{content}</div>
|
||||
)}
|
||||
</div>
|
||||
</SwipeMenu>
|
||||
<Collapsible.Content>
|
||||
{/* For lastInGroup check, the placeholder must be placed above all children in the dom */}
|
||||
<div className={styles.collapseContentPlaceholder}>
|
||||
|
||||
@@ -5,5 +5,6 @@ export * from './page-header';
|
||||
export * from './rename';
|
||||
export * from './search-input';
|
||||
export * from './search-result';
|
||||
export * from './swipe-menu';
|
||||
export * from './user-plan-tag';
|
||||
export * from './workspace-selector';
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||
import anime from 'animejs';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { HapticsService } from '../../modules/haptics';
|
||||
import { SwipeHelper } from '../../utils';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export interface SwipeMenuProps extends HTMLAttributes<HTMLDivElement> {
|
||||
menu: ReactNode;
|
||||
/**
|
||||
* if the swipe distance is greater than the threshold, will execute the callback
|
||||
* @default 200
|
||||
*/
|
||||
executeThreshold?: number;
|
||||
onExecute?: () => void;
|
||||
normalWidth?: number;
|
||||
}
|
||||
|
||||
type TickFunc<T extends Array<any> = any[]> = (
|
||||
content: HTMLDivElement,
|
||||
menu: HTMLDivElement,
|
||||
options: {
|
||||
deltaX: number;
|
||||
normalWidth: number;
|
||||
},
|
||||
...args: T
|
||||
) => void;
|
||||
|
||||
const tick: TickFunc = (content, menu, options) => {
|
||||
const limitedDeltaX = Math.min(
|
||||
0,
|
||||
Math.max(options.deltaX, -options.normalWidth * 3)
|
||||
);
|
||||
content.style.transform = `translateX(${limitedDeltaX}px)`;
|
||||
menu.style.transform = `translateX(${limitedDeltaX}px)`;
|
||||
menu.style.width = `${Math.max(options.normalWidth, Math.abs(limitedDeltaX))}px`;
|
||||
};
|
||||
|
||||
const animate: TickFunc<[number]> = (content, menu, options, to) => {
|
||||
const styleX = Number(content.style.transform.match(/-?\d+/)?.[0]);
|
||||
const deltaX = isNaN(styleX) ? options.deltaX : styleX;
|
||||
|
||||
const proxy = new Proxy(
|
||||
{ deltaX },
|
||||
{
|
||||
set(target, key, value) {
|
||||
if (key !== 'deltaX') return false;
|
||||
target.deltaX = value;
|
||||
tick(content, menu, { ...options, deltaX: value });
|
||||
return true;
|
||||
},
|
||||
}
|
||||
);
|
||||
if (deltaX === to) return;
|
||||
anime({
|
||||
targets: proxy,
|
||||
deltaX: to,
|
||||
duration: 230,
|
||||
easing: 'easeInOutSine',
|
||||
});
|
||||
};
|
||||
|
||||
const activeId$ = new LiveData<string | null>(null);
|
||||
|
||||
/**
|
||||
* Only support swipe left yet
|
||||
* Only support single menu item yet
|
||||
*/
|
||||
export const SwipeMenu = ({
|
||||
children,
|
||||
className,
|
||||
menu,
|
||||
normalWidth = 90,
|
||||
executeThreshold = 200,
|
||||
onExecute,
|
||||
...props
|
||||
}: SwipeMenuProps) => {
|
||||
const id = useId();
|
||||
const haptics = useService(HapticsService);
|
||||
const activeId = useLiveData(activeId$);
|
||||
|
||||
const isOpenRef = useRef(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
isOpenRef.current = false;
|
||||
const content = contentRef.current;
|
||||
const menu = menuRef.current;
|
||||
if (!content || !menu) return;
|
||||
animate(content, menu, { deltaX: -normalWidth, normalWidth }, 0);
|
||||
}, [normalWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeId && activeId !== id && isOpenRef.current) {
|
||||
handleClose();
|
||||
}
|
||||
}, [activeId, handleClose, id]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const menu = menuRef.current;
|
||||
const content = contentRef.current;
|
||||
if (!container || !menu || !content) return;
|
||||
const swipeHelper = new SwipeHelper();
|
||||
|
||||
let shouldExecute = false;
|
||||
|
||||
return swipeHelper.init(container, {
|
||||
preventScroll: true,
|
||||
direction: 'horizontal',
|
||||
onSwipeStart() {
|
||||
activeId$.next(id);
|
||||
},
|
||||
onSwipe({ deltaX: dragX, initialDirection }) {
|
||||
if (initialDirection !== 'horizontal') return;
|
||||
|
||||
const deltaX = isOpenRef.current ? dragX - normalWidth : dragX;
|
||||
const prevShouldExecute = shouldExecute;
|
||||
shouldExecute = Math.abs(deltaX) > executeThreshold;
|
||||
if (shouldExecute && !prevShouldExecute) {
|
||||
haptics.impact({ style: 'LIGHT' });
|
||||
}
|
||||
tick(content, menu, { deltaX, normalWidth });
|
||||
},
|
||||
onSwipeEnd({ deltaX: dragX, initialDirection }) {
|
||||
activeId$.next(null);
|
||||
if (initialDirection !== 'horizontal') return;
|
||||
|
||||
const deltaX = isOpenRef.current ? dragX - normalWidth : dragX;
|
||||
if (shouldExecute) {
|
||||
animate(content, menu, { deltaX, normalWidth }, 0);
|
||||
onExecute?.();
|
||||
isOpenRef.current = false;
|
||||
return;
|
||||
}
|
||||
// open
|
||||
if (Math.abs(deltaX) > normalWidth / 2) {
|
||||
animate(content, menu, { deltaX, normalWidth }, -normalWidth);
|
||||
isOpenRef.current = true;
|
||||
}
|
||||
// close
|
||||
else {
|
||||
animate(content, menu, { deltaX, normalWidth }, 0);
|
||||
isOpenRef.current = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [executeThreshold, haptics, id, normalWidth, onExecute]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.container, className)}
|
||||
ref={containerRef}
|
||||
{...props}
|
||||
>
|
||||
<div className={styles.content} ref={contentRef}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={styles.menu} ref={menuRef} onClick={handleClose}>
|
||||
{menu}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const content = style({
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
});
|
||||
export const menu = style({
|
||||
position: 'absolute',
|
||||
left: '100%',
|
||||
top: 0,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: cssVarV2('icon/primary'),
|
||||
background: cssVarV2('layer/background/mobile/tertiary'),
|
||||
});
|
||||
13
packages/frontend/core/src/mobile/modules/haptics/index.ts
Normal file
13
packages/frontend/core/src/mobile/modules/haptics/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { HapticProvider } from './providers/haptic';
|
||||
import { HapticsService } from './services/haptics';
|
||||
|
||||
export function configureMobileHapticsModule(framework: Framework) {
|
||||
framework.service(
|
||||
HapticsService,
|
||||
f => new HapticsService(f.getOptional(HapticProvider))
|
||||
);
|
||||
}
|
||||
|
||||
export { HapticProvider, HapticsService };
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createIdentifier } from '@toeverything/infra';
|
||||
|
||||
export interface HapticProvider {
|
||||
impact: (options?: { style?: 'HEAVY' | 'LIGHT' | 'MEDIUM' }) => Promise<void>;
|
||||
notification: (options?: {
|
||||
type?: 'SUCCESS' | 'ERROR' | 'WARNING';
|
||||
}) => Promise<void>;
|
||||
vibrate: (options?: { duration?: number }) => Promise<void>;
|
||||
selectionStart: () => Promise<void>;
|
||||
selectionChanged: () => Promise<void>;
|
||||
selectionEnd: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const HapticProvider =
|
||||
createIdentifier<HapticProvider>('HapticProvider');
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
import type { HapticProvider } from '../providers/haptic';
|
||||
|
||||
type ExtractArg<T extends keyof HapticProvider> = Parameters<
|
||||
HapticProvider[T]
|
||||
>[0];
|
||||
|
||||
export class HapticsService extends Service {
|
||||
constructor(private readonly provider?: HapticProvider) {
|
||||
super();
|
||||
}
|
||||
|
||||
impact(options?: ExtractArg<'impact'>) {
|
||||
this.provider?.impact?.(options)?.catch(console.error);
|
||||
}
|
||||
|
||||
notification(options?: ExtractArg<'notification'>) {
|
||||
this.provider?.notification?.(options)?.catch(console.error);
|
||||
}
|
||||
|
||||
vibrate(options?: ExtractArg<'vibrate'>) {
|
||||
this.provider?.vibrate?.(options)?.catch(console.error);
|
||||
}
|
||||
|
||||
selectionStart() {
|
||||
this.provider?.selectionStart?.().catch(console.error);
|
||||
}
|
||||
|
||||
selectionChanged() {
|
||||
this.provider?.selectionChanged?.().catch(console.error);
|
||||
}
|
||||
|
||||
selectionEnd() {
|
||||
this.provider?.selectionEnd?.().catch(console.error);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { configureMobileHapticsModule } from './haptics';
|
||||
import { configureMobileNavigationGestureModule } from './navigation-gesture';
|
||||
import { configureMobileSearchModule } from './search';
|
||||
import { configureMobileVirtualKeyboardModule } from './virtual-keyboard';
|
||||
@@ -8,4 +9,5 @@ export function configureMobileModules(framework: Framework) {
|
||||
configureMobileSearchModule(framework);
|
||||
configureMobileVirtualKeyboardModule(framework);
|
||||
configureMobileNavigationGestureModule(framework);
|
||||
configureMobileHapticsModule(framework);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SwipeHelper } from '@affine/core/mobile/utils';
|
||||
import anime from 'animejs';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
} from './constants';
|
||||
import { JournalDatePickerContext } from './context';
|
||||
import * as styles from './month.css';
|
||||
import { SwipeHelper } from './swipe-helper';
|
||||
import { getFirstDayOfMonth } from './utils';
|
||||
import { WeekRow } from './week';
|
||||
export interface MonthViewProps {
|
||||
@@ -106,9 +106,9 @@ export const MonthView = ({ viewportHeight }: MonthViewProps) => {
|
||||
setCursor(selected);
|
||||
}
|
||||
},
|
||||
onSwipeEnd: ({ deltaX }) => {
|
||||
onSwipeEnd: ({ deltaX, speedX }) => {
|
||||
setSwiping(false);
|
||||
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD) {
|
||||
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD || speedX > 100) {
|
||||
animateTo(deltaX > 0 ? -1 : 1);
|
||||
} else {
|
||||
animateTo(0);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useGlobalEvent } from '@affine/core/mobile/hooks/use-global-events';
|
||||
import { SwipeHelper } from '@affine/core/mobile/utils';
|
||||
import anime from 'animejs';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
} from './constants';
|
||||
import { JournalDatePickerContext } from './context';
|
||||
import { MonthView } from './month';
|
||||
import { SwipeHelper } from './swipe-helper';
|
||||
import * as styles from './viewport.css';
|
||||
import { WeekHeader, WeekRowSwipe } from './week';
|
||||
|
||||
@@ -97,8 +97,8 @@ export const ResizeViewport = ({
|
||||
setDragOffset(deltaY);
|
||||
setIsDragging(true);
|
||||
},
|
||||
onSwipeEnd: ({ deltaY }) => {
|
||||
if (Math.abs(deltaY) > 2 * CELL_HEIGHT) {
|
||||
onSwipeEnd: ({ deltaY, speedY }) => {
|
||||
if (Math.abs(deltaY) > 2 * CELL_HEIGHT || speedY > 100) {
|
||||
handleToggleModeWithAnimation(
|
||||
mode === 'week' ? 'month' : 'week',
|
||||
deltaY
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SwipeHelper } from '@affine/core/mobile/utils';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import anime from 'animejs';
|
||||
import clsx from 'clsx';
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
import { DATE_FORMAT, HORIZONTAL_SWIPE_THRESHOLD } from './constants';
|
||||
import { JournalDatePickerContext } from './context';
|
||||
import { DayCell } from './day-cell';
|
||||
import { SwipeHelper } from './swipe-helper';
|
||||
import { getFirstDayOfWeek } from './utils';
|
||||
import * as styles from './week.css';
|
||||
|
||||
@@ -115,10 +115,10 @@ export const WeekRowSwipe = ({ start }: WeekRowProps) => {
|
||||
setCursor(start);
|
||||
}
|
||||
},
|
||||
onSwipeEnd({ deltaX }) {
|
||||
onSwipeEnd({ deltaX, speedX }) {
|
||||
setSwiping(false);
|
||||
|
||||
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD) {
|
||||
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD || speedX > 100) {
|
||||
animateTo(deltaX > 0 ? -1 : 1);
|
||||
} else {
|
||||
animateTo(0);
|
||||
|
||||
1
packages/frontend/core/src/mobile/utils/index.ts
Normal file
1
packages/frontend/core/src/mobile/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './swipe-helper';
|
||||
@@ -1,3 +1,5 @@
|
||||
type Direction = 'horizontal' | 'vertical';
|
||||
|
||||
export interface SwipeInfo {
|
||||
e: TouchEvent;
|
||||
startX: number;
|
||||
@@ -7,25 +9,60 @@ export interface SwipeInfo {
|
||||
deltaX: number;
|
||||
deltaY: number;
|
||||
isFirst: boolean;
|
||||
/**
|
||||
* Instant horizontal speed in `px/s`
|
||||
*/
|
||||
speedX: number;
|
||||
/**
|
||||
* Average horizontal speed in `px/s`
|
||||
*/
|
||||
averageSpeedX: number;
|
||||
/**
|
||||
* Instant vertical speed in `px/s`
|
||||
*/
|
||||
speedY: number;
|
||||
/**
|
||||
* Average vertical speed in `px/s`
|
||||
*/
|
||||
averageSpeedY: number;
|
||||
/**
|
||||
* The first detected direction
|
||||
*/
|
||||
initialDirection: Direction | null;
|
||||
}
|
||||
|
||||
export interface SwipeHelperOptions {
|
||||
scope?: 'global' | 'inside';
|
||||
/**
|
||||
* When swipe started, the direction will be detected and not change until swipe ended.
|
||||
* If the direction is specified, and not match the detected one, the scroll won't be prevented.
|
||||
*/
|
||||
direction?: Direction;
|
||||
/**
|
||||
* @description The distance in px that determine which direction the swipe gesture is
|
||||
* @default 10
|
||||
*/
|
||||
directionThreshold?: number;
|
||||
preventScroll?: boolean;
|
||||
onTap?: () => void;
|
||||
onSwipeStart?: () => void;
|
||||
onSwipe?: (info: SwipeInfo) => void;
|
||||
onSwipeEnd?: (info: SwipeInfo) => void;
|
||||
}
|
||||
const defaultOptions = Object.freeze({
|
||||
scope: 'global',
|
||||
directionThreshold: 10,
|
||||
} as const);
|
||||
|
||||
export class SwipeHelper {
|
||||
private _trigger: HTMLElement | null = null;
|
||||
private _options: SwipeHelperOptions = {
|
||||
scope: 'global',
|
||||
};
|
||||
private _options: SwipeHelperOptions = defaultOptions;
|
||||
private _direction: Direction | null = null;
|
||||
private _startPos: { x: number; y: number } = { x: 0, y: 0 };
|
||||
private _isFirst: boolean = true;
|
||||
private _lastInfo: SwipeInfo | null = null;
|
||||
private _startTime: number = 0;
|
||||
private _lastTime: number = 0;
|
||||
|
||||
get scopeElement() {
|
||||
return this._options.scope === 'inside'
|
||||
@@ -69,6 +106,8 @@ export class SwipeHelper {
|
||||
x: touch.clientX,
|
||||
y: touch.clientY,
|
||||
};
|
||||
this._lastTime = Date.now();
|
||||
this._startTime = this._lastTime;
|
||||
this._options.onSwipeStart?.();
|
||||
const moveHandler = this._handleTouchMove.bind(this);
|
||||
this.scopeElement.addEventListener('touchmove', moveHandler, {
|
||||
@@ -87,10 +126,18 @@ export class SwipeHelper {
|
||||
}
|
||||
|
||||
private _handleTouchMove(e: TouchEvent) {
|
||||
if (this._options.preventScroll) {
|
||||
const info = this._getInfo(e);
|
||||
if (
|
||||
this._options.preventScroll &&
|
||||
// direction is not detected
|
||||
(!this._direction ||
|
||||
// direction is not specified
|
||||
!this._options.direction ||
|
||||
// direction is same as specified
|
||||
this._direction === this._options.direction)
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
const info = this._getInfo(e);
|
||||
this._lastInfo = info;
|
||||
this._isFirst = false;
|
||||
this._options.onSwipe?.(info);
|
||||
@@ -110,9 +157,30 @@ export class SwipeHelper {
|
||||
}
|
||||
|
||||
private _getInfo(e: TouchEvent): SwipeInfo {
|
||||
const lastTime = this._lastTime;
|
||||
this._lastTime = Date.now();
|
||||
const deltaTime = this._lastTime - lastTime;
|
||||
const touch = e.touches[0];
|
||||
const deltaX = touch.clientX - this._startPos.x;
|
||||
const deltaY = touch.clientY - this._startPos.y;
|
||||
const speedX =
|
||||
(Math.abs(deltaX - (this._lastInfo?.deltaX ?? 0)) / deltaTime) * 1000;
|
||||
const averageSpeedX =
|
||||
(Math.abs(deltaX) / (this._lastTime - this._startTime)) * 1000;
|
||||
const speedY =
|
||||
(Math.abs(deltaY - (this._lastInfo?.deltaY ?? 0)) / deltaTime) * 1000;
|
||||
const averageSpeedY =
|
||||
(Math.abs(deltaY) / (this._lastTime - this._startTime)) * 1000;
|
||||
|
||||
// detect direction
|
||||
const threshold =
|
||||
this._options.directionThreshold ?? defaultOptions.directionThreshold;
|
||||
const maxDelta = Math.max(Math.abs(deltaX), Math.abs(deltaY));
|
||||
if (!this._direction && maxDelta > threshold) {
|
||||
this._direction =
|
||||
Math.abs(deltaX) > Math.abs(deltaY) ? 'horizontal' : 'vertical';
|
||||
}
|
||||
|
||||
return {
|
||||
e,
|
||||
startX: this._startPos.x,
|
||||
@@ -122,11 +190,19 @@ export class SwipeHelper {
|
||||
deltaX,
|
||||
deltaY,
|
||||
isFirst: this._isFirst,
|
||||
speedX,
|
||||
averageSpeedX,
|
||||
speedY,
|
||||
averageSpeedY,
|
||||
initialDirection: this._direction,
|
||||
};
|
||||
}
|
||||
|
||||
private _clearDrag() {
|
||||
this._lastInfo = null;
|
||||
this._lastTime = 0;
|
||||
this._startTime = 0;
|
||||
this._direction = null;
|
||||
this._dragMoveCleanup();
|
||||
this._dragEndCleanup();
|
||||
}
|
||||
Reference in New Issue
Block a user