TelegramPWA/src/components/mediaViewer/MediaViewerSlides.tsx
2022-08-17 10:38:18 +02:00

750 lines
25 KiB
TypeScript

import type { FC } from '../../lib/teact/teact';
import React, {
memo, useCallback, useEffect, useRef, useState,
} from '../../lib/teact/teact';
import type { MediaViewerOrigin } from '../../types';
import type { RealTouchEvent } from '../../util/captureEvents';
import { animateNumber, timingFunctions } from '../../util/animation';
import buildClassName from '../../util/buildClassName';
import { captureEvents, IOS_SCREEN_EDGE_THRESHOLD } from '../../util/captureEvents';
import { IS_IOS, IS_TOUCH_ENV } from '../../util/environment';
import { clamp, isBetween, round } from '../../util/math';
import { debounce } from '../../util/schedulers';
import useDebouncedCallback from '../../hooks/useDebouncedCallback';
import useForceUpdate from '../../hooks/useForceUpdate';
import useLang from '../../hooks/useLang';
import usePrevious from '../../hooks/usePrevious';
import useTimeout from '../../hooks/useTimeout';
import useWindowSize from '../../hooks/useWindowSize';
import MediaViewerContent from './MediaViewerContent';
import './MediaViewerSlides.scss';
const { easeOutCubic, easeOutQuart } = timingFunctions;
type OwnProps = {
mediaId?: number;
getMediaId: (fromId?: number, direction?: number) => number | undefined;
isVideo?: boolean;
isGif?: boolean;
isPhoto?: boolean;
isOpen?: boolean;
selectMedia: (id?: number) => void;
chatId?: string;
threadId?: number;
avatarOwnerId?: string;
origin?: MediaViewerOrigin;
animationLevel: 0 | 1 | 2;
onClose: () => void;
hasFooter?: boolean;
onFooterClick: () => void;
zoomLevelChange: number;
};
const SWIPE_X_THRESHOLD = 50;
const SWIPE_Y_THRESHOLD = 50;
const SLIDES_GAP = IS_TOUCH_ENV ? 40 : 0;
const ANIMATION_DURATION = 350;
const DEBOUNCE_MESSAGE = 350;
const DEBOUNCE_SWIPE = 500;
const DEBOUNCE_ACTIVE = 800;
const DOUBLE_TAP_ZOOM = 3;
const CLICK_Y_THRESHOLD = 80;
const HEADER_HEIGHT = 60;
const MAX_ZOOM = 4;
const MIN_ZOOM = 1;
let cancelAnimation: Function | undefined;
let cancelZoomAnimation: Function | undefined;
type Transform = {
x: number;
y: number;
scale: number;
};
enum SwipeDirection {
Horizontal,
Vertical,
}
const MediaViewerSlides: FC<OwnProps> = ({
mediaId,
getMediaId,
selectMedia,
isVideo,
isGif,
isPhoto,
isOpen,
hasFooter,
zoomLevelChange,
animationLevel,
...rest
}) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const activeSlideRef = useRef<HTMLDivElement>(null);
const transformRef = useRef<Transform>({ x: 0, y: 0, scale: 1 });
const lastTransformRef = useRef<Transform>({ x: 0, y: 0, scale: 1 });
const swipeDirectionRef = useRef<SwipeDirection | undefined>(undefined);
const isActiveRef = useRef(true);
const isReleasedRef = useRef(false);
const [activeMediaId, setActiveMediaId] = useState<number | undefined>(mediaId);
const prevZoomLevelChange = usePrevious(zoomLevelChange);
const hasZoomChanged = prevZoomLevelChange !== undefined && prevZoomLevelChange !== zoomLevelChange;
const forceUpdate = useForceUpdate();
const [areControlsVisible, setControlsVisible] = useState(false);
const [isMouseDown, setIsMouseDown] = useState(false);
const { height: windowHeight, width: windowWidth, isResizing } = useWindowSize();
const { onClose } = rest;
const lang = useLang();
const setTransform = useCallback((value: Transform) => {
transformRef.current = value;
forceUpdate();
}, [forceUpdate]);
const selectMediaDebounced = useDebouncedCallback(selectMedia, [], DEBOUNCE_MESSAGE, true);
const clearSwipeDirectionDebounced = useDebouncedCallback(() => {
swipeDirectionRef.current = undefined;
}, [], DEBOUNCE_SWIPE, true);
const setIsActiveDebounced = useDebouncedCallback((value: boolean) => {
isActiveRef.current = value;
forceUpdate();
}, [forceUpdate], DEBOUNCE_ACTIVE, true);
const shouldCloseOnVideo = isGif && !IS_IOS;
const clickXThreshold = IS_TOUCH_ENV ? 40 : windowWidth / 10;
const handleControlsVisibility = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (!IS_TOUCH_ENV) return;
const isFooter = windowHeight - e.pageY < CLICK_Y_THRESHOLD;
if (!isFooter && e.pageX < clickXThreshold) return;
if (!isFooter && e.pageX > windowWidth - clickXThreshold) return;
setControlsVisible(!areControlsVisible);
}, [clickXThreshold, areControlsVisible, windowHeight, windowWidth]);
useTimeout(() => setControlsVisible(true), ANIMATION_DURATION + 100);
useEffect(() => {
if (!containerRef.current || activeMediaId === undefined) {
return undefined;
}
let lastTransform = lastTransformRef.current;
const lastDragOffset = {
x: 0,
y: 0,
};
const lastZoomCenter = {
x: 0,
y: 0,
};
const panDelta = {
x: 0,
y: 0,
};
let lastGestureTime = Date.now();
let initialContentRect: DOMRect;
let content: HTMLElement | null;
const setLastGestureTime = debounce(() => {
lastGestureTime = Date.now();
}, 500, false, true);
const changeSlide = (direction: number) => {
const mId = getMediaId(activeMediaId, direction);
if (mId !== undefined) {
const offset = (windowWidth + SLIDES_GAP) * direction;
transformRef.current.x += offset;
isActiveRef.current = false;
setActiveMediaId(mId);
selectMediaDebounced(mId);
setIsActiveDebounced(true);
lastTransform = { x: 0, y: 0, scale: 1 };
if (animationLevel === 0) {
setTransform(lastTransform);
return true;
}
cancelAnimation = animateNumber({
from: transformRef.current.x,
to: 0,
duration: ANIMATION_DURATION,
timing: easeOutCubic,
onUpdate: (value) => setTransform({
y: 0,
x: value,
scale: 1,
}),
});
return true;
}
return false;
};
const changeSlideOnClick = (e: MouseEvent): [boolean, boolean] => {
if (transformRef.current.scale !== 1) return [false, false];
let direction = 0;
if (windowHeight - e.pageY < CLICK_Y_THRESHOLD) {
return [false, false];
}
if (e.pageX < clickXThreshold) {
direction = -1;
} else if (e.pageX > windowWidth - clickXThreshold) {
direction = 1;
}
const hasNextSlide = changeSlide(direction);
const isInThreshold = direction !== 0;
return [isInThreshold, hasNextSlide];
};
const handleKeyDown = (e: KeyboardEvent) => {
if (transformRef.current.scale !== 1) return;
switch (e.key) {
case 'Left': // IE/Edge specific value
case 'ArrowLeft':
changeSlide(-1);
break;
case 'Right': // IE/Edge specific value
case 'ArrowRight':
changeSlide(1);
break;
}
};
const calculateOffsetBoundaries = (
{ x, y, scale }: Transform,
offsetTop = 0,
):[Transform, boolean, boolean] => {
if (!initialContentRect) return [{ x, y, scale }, true, true];
// Get current content boundaries
let inBoundsX = true;
let inBoundsY = true;
const centerX = (windowWidth - windowWidth * scale) / 2;
const centerY = (windowHeight - windowHeight * scale) / 2;
// If content is outside window we calculate offset boundaries
// based on initial content rect and current scale
const minOffsetX = Math.max(-initialContentRect.left * scale, centerX);
const maxOffsetX = windowWidth - initialContentRect.right * scale;
inBoundsX = isBetween(x, maxOffsetX, minOffsetX);
x = clamp(x, maxOffsetX, minOffsetX);
const minOffsetY = Math.max(-initialContentRect.top * scale + offsetTop, centerY);
const maxOffsetY = windowHeight - initialContentRect.bottom * scale;
inBoundsY = isBetween(y, maxOffsetY, minOffsetY);
y = clamp(y, maxOffsetY, minOffsetY);
return [{ x, y, scale }, inBoundsX, inBoundsY];
};
const onRelease = (e: MouseEvent | RealTouchEvent | WheelEvent) => {
// This allows to prevent onRelease triggered by debounced wheel event
// after onRelease was triggered manually in onDrag
if (isReleasedRef.current) {
isReleasedRef.current = false;
return;
}
if (e.type === 'mouseup') {
setIsMouseDown(false);
}
const absX = Math.abs(transformRef.current.x);
const absY = Math.abs(transformRef.current.y);
const {
scale,
x,
y,
} = transformRef.current;
clearSwipeDirectionDebounced();
setIsActiveDebounced(true);
// If scale is less than 1 we need to bounce back
if (scale < 1) {
lastTransform = { x: 0, y: 0, scale: 1 };
cancelAnimation = animateNumber({
from: [x, y, scale],
to: [0, 0, 1],
duration: ANIMATION_DURATION,
timing: easeOutCubic,
onUpdate: (value) => setTransform({
x: value[0],
y: value[1],
scale: value[2],
}),
});
return;
}
if (scale > 1) {
// Get current content boundaries
const s1 = Math.min(scale, MAX_ZOOM);
const scaleFactor = s1 / scale;
// Calculate new position based on the last zoom center to keep the zoom center
// at the same position when bouncing back from max zoom
let x1 = x * scaleFactor + (lastZoomCenter.x - scaleFactor * lastZoomCenter.x);
let y1 = y * scaleFactor + (lastZoomCenter.y - scaleFactor * lastZoomCenter.y);
// Arbitrary pan velocity coefficient
const k = 0.15;
// If scale didn't change, we need to add inertia to pan gesture
if (e.type !== 'wheel' && lastTransform.scale === scale) {
// Calculate user gesture velocity
const Vx = Math.abs(lastDragOffset.x) / (Date.now() - lastGestureTime);
const Vy = Math.abs(lastDragOffset.y) / (Date.now() - lastGestureTime);
// Add extra distance based on gesture velocity and last pan delta
x1 -= Math.abs(lastDragOffset.x) * Vx * k * panDelta.x;
y1 -= Math.abs(lastDragOffset.y) * Vy * k * panDelta.y;
}
[lastTransform] = calculateOffsetBoundaries({ x: x1, y: y1, scale: s1 }, HEADER_HEIGHT);
cancelAnimation = animateNumber({
from: [x, y, scale],
to: [lastTransform.x, lastTransform.y, lastTransform.scale],
duration: ANIMATION_DURATION,
timing: easeOutCubic,
onUpdate: (value) => setTransform({
x: value[0],
y: value[1],
scale: value[2],
}),
});
return;
}
lastTransform = {
x,
y,
scale,
};
if (absY >= SWIPE_Y_THRESHOLD) {
onClose();
return;
}
// Bounce back if vertical swipe is below threshold
if (absY > 0) {
cancelAnimation = animateNumber({
from: y,
to: 0,
duration: ANIMATION_DURATION,
timing: easeOutCubic,
onUpdate: (value) => setTransform({
x: 0,
y: value,
scale,
}),
});
return;
}
// Get horizontal swipe direction
const direction = x < 0 ? 1 : -1;
const mId = getMediaId(activeMediaId, x < 0 ? 1 : -1);
// Get the direction of the last pan gesture.
// Could be different from the total horizontal swipe direction
// if user starts a swipe in one direction and then changes the direction
// we need to cancel slide transition
const dirX = panDelta.x < 0 ? -1 : 1;
if (mId !== undefined && absX >= SWIPE_X_THRESHOLD && direction === dirX) {
const offset = (windowWidth + SLIDES_GAP) * direction;
// If image is shifted by more than SWIPE_X_THRESHOLD,
// We shift everything by one screen width and then set new active message id
transformRef.current.x += offset;
setActiveMediaId(mId);
selectMediaDebounced(mId);
}
// Then we always return to the original position
cancelAnimation = animateNumber({
from: transformRef.current.x,
to: 0,
duration: ANIMATION_DURATION,
timing: easeOutCubic,
onUpdate: (value) => setTransform({
y: 0,
x: value,
scale: transformRef.current.scale,
}),
});
};
const cleanup = captureEvents(containerRef.current, {
isNotPassive: true,
withNativeDrag: true,
excludedClosestSelector: '.MediaViewerFooter, .ZoomControls',
minZoom: MIN_ZOOM,
maxZoom: MAX_ZOOM,
doubleTapZoom: DOUBLE_TAP_ZOOM,
onCapture: (e) => {
if (checkIfControlTarget(e)) return;
if (e.type === 'mousedown') {
setIsMouseDown(true);
if (transformRef.current.scale !== 1) {
e.preventDefault();
return;
}
}
lastGestureTime = Date.now();
const { x, y, scale } = transformRef.current;
if (x === 0 && y === 0 && scale === 1) {
if (!activeSlideRef.current) return;
content = activeSlideRef.current.querySelector('img, video');
if (!content) return;
// Store initial content rect, without transformations
initialContentRect = content.getBoundingClientRect();
}
},
onDrag: (event, captureEvent, {
dragOffsetX,
dragOffsetY,
}, cancelDrag) => {
if (isReleasedRef.current || checkIfControlTarget(event)) return;
// Avoid conflicts with swipe-to-back gestures
if (IS_IOS && captureEvent.type === 'touchstart') {
const { pageX } = (captureEvent as RealTouchEvent).touches[0];
if (pageX <= IOS_SCREEN_EDGE_THRESHOLD || pageX >= windowWidth - IOS_SCREEN_EDGE_THRESHOLD) {
return;
}
}
if (cancelAnimation) {
cancelAnimation();
cancelAnimation = undefined;
}
panDelta.x = lastDragOffset.x - dragOffsetX;
panDelta.y = lastDragOffset.y - dragOffsetY;
lastDragOffset.x = dragOffsetX;
lastDragOffset.y = dragOffsetY;
const absOffsetX = Math.abs(dragOffsetX);
const absOffsetY = Math.abs(dragOffsetY);
const {
scale,
x,
y,
} = transformRef.current;
const threshold = 10;
const tolerance = 1.5;
// If user is inactive but is still touching the screen
// we reset last gesture time
setLastGestureTime();
// If image is scaled we just need to pan it
if (scale !== 1) {
const x1 = lastTransform.x + dragOffsetX;
const y1 = lastTransform.y + dragOffsetY;
if (['wheel', 'mousemove'].includes(event.type)) {
const [transform, inBoundsX, inBoundsY] = calculateOffsetBoundaries({ x: x1, y: y1, scale }, HEADER_HEIGHT);
if (cancelDrag) cancelDrag(!inBoundsX, !inBoundsY);
setTransform(transform);
return;
}
if ('touches' in event && event.touches.length === 1) {
setTransform({
x: x1,
y: y1,
scale,
});
}
return;
}
if (event.type === 'mousemove') return;
if (swipeDirectionRef.current !== SwipeDirection.Vertical) {
// If user is swiping horizontally or horizontal shift is dominant
// we change only horizontal position
if (swipeDirectionRef.current === SwipeDirection.Horizontal
|| Math.abs(x) > threshold || absOffsetX / absOffsetY > tolerance) {
swipeDirectionRef.current = SwipeDirection.Horizontal;
isActiveRef.current = false;
const limit = windowWidth + SLIDES_GAP;
const x1 = clamp(dragOffsetX, -limit, limit);
setTransform({
x: x1,
y: 0,
scale,
});
// We know that at this point onRelease will trigger slide change,
// We can trigger onRelease directly instead of waiting for the debounced callback
// to avoid a delay
if (event.type === 'wheel' && Math.abs(x1) > SWIPE_X_THRESHOLD * 2) {
onRelease(event);
isReleasedRef.current = true;
}
return;
}
}
// If vertical shift is dominant we change only vertical position
if (swipeDirectionRef.current === SwipeDirection.Vertical
|| Math.abs(y) > threshold || absOffsetY / absOffsetX > tolerance) {
swipeDirectionRef.current = SwipeDirection.Vertical;
const limit = windowHeight;
const y1 = clamp(dragOffsetY, -limit, limit);
setTransform({
x: 0,
y: y1,
scale,
});
if (event.type === 'wheel' && Math.abs(y1) > SWIPE_Y_THRESHOLD * 2) {
onRelease(event);
isReleasedRef.current = true;
}
}
},
onZoom: (e, {
zoom,
zoomFactor,
initialCenterX,
initialCenterY,
dragOffsetX,
dragOffsetY,
currentCenterX,
currentCenterY,
}) => {
if (cancelAnimation) cancelAnimation();
initialCenterX = initialCenterX || windowWidth / 2;
initialCenterY = initialCenterY || windowHeight / 2;
currentCenterX = currentCenterX || windowWidth / 2;
currentCenterY = currentCenterY || windowHeight / 2;
// Calculate current scale based on zoom factor and limits, add zoom margin for bounce back effect
const scale = zoom ?? clamp(lastTransform.scale * zoomFactor!, MIN_ZOOM * 0.5, MAX_ZOOM * 3);
const scaleFactor = scale / lastTransform.scale;
const offsetX = Math.abs(Math.min(lastTransform.x, 0));
const offsetY = Math.abs(Math.min(lastTransform.y, 0));
// Save last zoom center for bounce back effect
lastZoomCenter.x = currentCenterX;
lastZoomCenter.y = currentCenterY;
// Calculate new center relative to the shifted image
const scaledCenterX = offsetX + initialCenterX;
const scaledCenterY = offsetY + initialCenterY;
// Calculate how much we need to shift the image to keep the zoom center at the same position
const scaleOffsetX = (scaledCenterX - scaleFactor * scaledCenterX);
const scaleOffsetY = (scaledCenterY - scaleFactor * scaledCenterY);
const [transform] = calculateOffsetBoundaries({
x: lastTransform.x + scaleOffsetX + dragOffsetX,
y: lastTransform.y + scaleOffsetY + dragOffsetY,
scale,
});
setTransform(transform);
},
onClick(e) {
const [isInThreshold, hasNextSlide] = changeSlideOnClick(e as MouseEvent);
if (isInThreshold) {
e.preventDefault();
e.stopPropagation();
if (IS_TOUCH_ENV) return;
if (!hasNextSlide) onClose();
return;
}
if (lastTransform.scale !== 1 || IS_TOUCH_ENV) return;
if (shouldCloseOnVideo || !checkIfInsideSelector(e.target as HTMLElement, '.VideoPlayer')) {
onClose();
}
},
onDoubleClick(e, {
centerX,
centerY,
}) {
const [isInThreshold] = changeSlideOnClick(e as MouseEvent);
if (isInThreshold) {
e.preventDefault();
e.stopPropagation();
return;
}
if (!IS_TOUCH_ENV && e.type !== 'wheel') return;
const { x, y, scale } = transformRef.current;
// Calculate how much we need to shift the image to keep the zoom center at the same position
const scaleOffsetX = (centerX - DOUBLE_TAP_ZOOM * centerX);
const scaleOffsetY = (centerY - DOUBLE_TAP_ZOOM * centerY);
if (scale === 1) {
if (x !== 0 || y !== 0) return;
lastTransform = calculateOffsetBoundaries({
x: scaleOffsetX,
y: scaleOffsetY,
scale: DOUBLE_TAP_ZOOM,
})[0];
} else {
lastTransform = {
x: 0,
y: 0,
scale: 1,
};
}
cancelAnimation = animateNumber({
from: [x, y, scale],
to: [lastTransform.x, lastTransform.y, lastTransform.scale],
duration: ANIMATION_DURATION,
timing: easeOutCubic,
onUpdate: (value) => {
const transform = {
x: value[0],
y: value[1],
scale: value[2],
};
setTransform(transform);
},
});
},
onRelease,
});
document.addEventListener('keydown', handleKeyDown, false);
return () => {
cleanup();
document.removeEventListener('keydown', handleKeyDown, false);
};
}, [
onClose,
setTransform,
getMediaId,
activeMediaId,
windowWidth,
windowHeight,
clickXThreshold,
shouldCloseOnVideo,
selectMediaDebounced,
setIsActiveDebounced,
clearSwipeDirectionDebounced,
animationLevel,
setIsMouseDown,
]);
useEffect(() => {
if (!containerRef.current || !hasZoomChanged) return;
const { scale } = transformRef.current;
const dir = zoomLevelChange > 0 ? -1 : +1;
const minZoom = MIN_ZOOM * 0.6;
const maxZoom = MAX_ZOOM * 3;
let steps = 100;
let prevValue = 0;
if (scale <= minZoom && dir > 0) return;
if (scale >= maxZoom && dir < 0) return;
if (scale === 1 && dir > 0) steps = 20;
if (cancelZoomAnimation) cancelZoomAnimation();
cancelZoomAnimation = animateNumber({
from: dir,
to: dir * steps,
duration: ANIMATION_DURATION,
timing: easeOutQuart,
onUpdate: (value) => {
if (!containerRef.current) return;
const delta = round(value - prevValue, 2);
prevValue = value;
// To reuse existing logic we trigger wheel event for zoom buttons
const wheelEvent = new WheelEvent('wheel', {
deltaY: delta,
ctrlKey: true,
});
containerRef.current.dispatchEvent(wheelEvent);
},
});
}, [zoomLevelChange, hasZoomChanged]);
if (activeMediaId === undefined) return undefined;
const nextMediaId = getMediaId(activeMediaId, 1);
const prevMediaId = getMediaId(activeMediaId, -1);
const hasPrev = prevMediaId !== undefined;
const hasNext = nextMediaId !== undefined;
const offsetX = transformRef.current.x;
const offsetY = transformRef.current.y;
const { scale } = transformRef.current;
return (
<div className="MediaViewerSlides" ref={containerRef}>
{hasPrev && scale === 1 && !isResizing && (
<div className="MediaViewerSlide" style={getAnimationStyle(-windowWidth + offsetX - SLIDES_GAP)}>
<MediaViewerContent
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...rest}
animationLevel={animationLevel}
areControlsVisible={areControlsVisible}
mediaId={prevMediaId}
/>
</div>
)}
<div
className={buildClassName(
'MediaViewerSlide',
'MediaViewerSlide--active',
isMouseDown && scale > 1 && 'MediaViewerSlide--moving',
)}
onClick={handleControlsVisibility}
ref={activeSlideRef}
style={getAnimationStyle(offsetX, offsetY, scale)}
>
<MediaViewerContent
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...rest}
mediaId={activeMediaId}
animationLevel={animationLevel}
isActive={isActiveRef.current}
setControlsVisible={setControlsVisible}
areControlsVisible={areControlsVisible && scale === 1}
/>
</div>
{hasNext && scale === 1 && !isResizing && (
<div className="MediaViewerSlide" style={getAnimationStyle(windowWidth + offsetX + SLIDES_GAP)}>
<MediaViewerContent
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...rest}
animationLevel={animationLevel}
areControlsVisible={areControlsVisible}
mediaId={nextMediaId}
/>
</div>
)}
{hasPrev && scale === 1 && !IS_TOUCH_ENV && (
<button
type="button"
className={`navigation prev ${isVideo && !isGif && 'inline'}`}
aria-label={lang('AccDescrPrevious')}
dir={lang.isRtl ? 'rtl' : undefined}
/>
)}
{hasNext && scale === 1 && !IS_TOUCH_ENV && (
<button
type="button"
className={`navigation next ${isVideo && !isGif && 'inline'}`}
aria-label={lang('Next')}
dir={lang.isRtl ? 'rtl' : undefined}
/>
)}
</div>
);
};
export default memo(MediaViewerSlides);
function getAnimationStyle(x = 0, y = 0, scale = 1) {
return `transform: translate3d(${x.toFixed(3)}px, ${y.toFixed(3)}px, 0px) scale(${scale.toFixed(3)});`;
}
function checkIfInsideSelector(element: HTMLElement, selector: string) {
if (!element) return false;
if (element.matches(selector)) return true;
return Boolean(element.closest(selector));
}
function checkIfControlTarget(e: TouchEvent | MouseEvent) {
const target = e.target as HTMLElement;
if (checkIfInsideSelector(target, '.VideoPlayerControls')) {
if (checkIfInsideSelector(
target,
'.play, .fullscreen, .volume, .volume-slider, .playback-rate, .playback-rate-menu',
)) {
return true;
}
e.preventDefault();
return true;
}
return false;
}