Merge branch 'feature/new-viewer' of github.com:ONLYOFFICE/DocSpace into feature/new-viewer

This commit is contained in:
DmitrySychugov 2023-01-11 16:39:57 +05:00
commit 15fd6c83f7
4 changed files with 338 additions and 68 deletions

View File

@ -19,6 +19,8 @@ import {
TapCallback,
UP,
Vector2,
Tuple,
Point,
} from "./types";
const defaultProps: ConfigurationOptions = {
@ -36,6 +38,8 @@ const initialState: SwipeableState = {
start: 0,
swiping: false,
xy: [0, 0],
lastDistance: 0,
pinching: false,
};
const mouseMove = "mousemove";
const mouseUp = "mouseup";
@ -60,6 +64,26 @@ function getDirection(
return UP;
}
function getDistance(p1: Point, p2: Point): number {
return Math.hypot(p2.x - p1.x, p2.y - p1.y);
}
function getXYfromEvent(event: TouchEvent): Tuple<Point> {
return [...event.touches]
.map((touch) =>
({ x: touch.pageX, y: touch.pageY })
) as Tuple<Point>;
}
function getMiddleSegment(p1: Point, p2: Point): Point {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
}
}
function rotateXYByAngle(pos: Vector2, angle: number): Vector2 {
if (angle === 0) return pos;
const angleInRadians = (Math.PI / 180) * angle;
@ -74,16 +98,30 @@ function getHandlers(
set: Setter,
handlerProps: { trackMouse: boolean | undefined }
): [
{
ref: (element: HTMLElement | null) => void;
onMouseDown?: (event: React.MouseEvent) => void;
},
AttachTouch
] {
{
ref: (element: HTMLElement | null) => void;
onMouseDown?: (event: React.MouseEvent) => void;
},
AttachTouch
] {
const onStart = (event: HandledEvents) => {
const isTouch = "touches" in event;
// if more than a single touch don't track, for now...
if (isTouch && event.touches.length > 1) return;
if (isTouch && event.touches.length > 1) {
if (event.touches.length === 2) {
set((state) => {
const startPosition = getXYfromEvent(event);
return {
...state,
startPosition,
lastDistance: 0,
pinching: true,
}
})
} else {
return;
}
}
set((state, props) => {
// setup mouse listeners on document to track swipe since swipe can leave container
@ -113,6 +151,35 @@ function getHandlers(
// Discount a swipe if additional touches are present after
// a swipe has started.
if (isTouch && event.touches.length > 1) {
if (event.touches.length === 2) {
const touchFist = event.touches[0];
const touchSecond = event.touches[1];
const move = getXYfromEvent(event);
const middleSegment = getMiddleSegment(
{ x: touchFist.clientX, y: touchFist.clientY },
{ x: touchSecond.clientX, y: touchSecond.clientY }
)
const distance = getDistance(
{ x: touchFist.clientX, y: touchFist.clientY },
{ x: touchSecond.clientX, y: touchSecond.clientY }
)
if (state.lastDistance === 0) {
state.lastDistance = distance;
}
const scale = distance / state.lastDistance;
// console.log("move", move);
props.onZoom?.({ event, scale, middleSegment });
return {
...state,
lastDistance: distance,
}
}
return state;
}
@ -138,7 +205,7 @@ function getHandlers(
typeof props.delta === "number"
? props.delta
: props.delta[dir.toLowerCase() as Lowercase<SwipeDirections>] ||
defaultProps.delta;
defaultProps.delta;
if (absX < delta && absY < delta && !state.swiping) return state;
const eventData = {
@ -152,6 +219,7 @@ function getHandlers(
initial: state.initial,
velocity,
vxvy,
piching: state.pinching,
};
// call onSwipeStart if present and is first swipe event
@ -201,7 +269,7 @@ function getHandlers(
const onSwipedDir =
props[
`onSwiped${eventData.dir}` as keyof SwipeableDirectionCallbacks
`onSwiped${eventData.dir}` as keyof SwipeableDirectionCallbacks
];
onSwipedDir && onSwipedDir(eventData);
}
@ -239,7 +307,7 @@ function getHandlers(
*
*/
const attachTouch: AttachTouch = (el, props) => {
let cleanup = () => {};
let cleanup = () => { };
if (el && el.addEventListener) {
const baseOptions = {
...defaultProps.touchEventOptions,
@ -251,18 +319,18 @@ function getHandlers(
(e: HandledEvents) => void,
{ passive: boolean }
][] = [
[touchStart, onStart, baseOptions],
// preventScrollOnSwipe option supersedes touchEventOptions.passive
[
touchMove,
onMove,
{
...baseOptions,
...(props.preventScrollOnSwipe ? { passive: false } : {}),
},
],
[touchEnd, onEnd, baseOptions],
];
[touchStart, onStart, baseOptions],
// preventScrollOnSwipe option supersedes touchEventOptions.passive
[
touchMove,
onMove,
{
...baseOptions,
...(props.preventScrollOnSwipe ? { passive: false } : {}),
},
],
[touchEnd, onEnd, baseOptions],
];
tls.forEach(([e, h, o]) => el.addEventListener(e, h, o));
// return properly scoped cleanup method for removing listeners, options not required
cleanup = () => tls.forEach(([e, h]) => el.removeEventListener(e, h));
@ -381,10 +449,10 @@ export function useSwipeable(options) {
() =>
getHandlers(
(stateSetter) =>
(transientState.current = stateSetter(
transientState.current,
transientProps.current
)),
(transientState.current = stateSetter(
transientState.current,
transientProps.current
)),
{ trackMouse }
),
[trackMouse]

View File

@ -6,6 +6,15 @@ export const UP = "Up";
export const DOWN = "Down";
export type HandledEvents = React.MouseEvent | TouchEvent | MouseEvent;
export type Vector2 = [number, number];
export type Tuple<T> = [T, T];
export type Point = { x: number; y: number }
export type ZoomEvent = {
event: TouchEvent,
scale: number,
middleSegment: Point,
}
export type SwipeDirections =
| typeof LEFT
| typeof RIGHT
@ -52,10 +61,13 @@ export interface SwipeEventData {
* Velocity per axis - [ deltaX/time, deltaY/time ]
*/
vxvy: Vector2;
piching: boolean;
}
export type SwipeCallback = (eventData: SwipeEventData) => void;
export type TapCallback = ({ event }: { event: HandledEvents }) => void;
export type ZoomCallback = (event: ZoomEvent) => void;
export type SwipeableDirectionCallbacks = {
/**
@ -95,7 +107,7 @@ export type SwipeableCallbacks = SwipeableDirectionCallbacks & {
onTap: TapCallback;
// TODO: add zoom functionality
onZoom: TapCallback;
onZoom: ZoomCallback;
/**
* Called for `touchstart` and `mousedown`.
*/
@ -161,6 +173,9 @@ export type SwipeableState = {
start: number;
swiping: boolean;
xy: Vector2;
startPosition?: Tuple<Point>;
lastDistance: number;
pinching: boolean;
};
export type StateSetter = (

View File

@ -486,6 +486,27 @@ const ViewerBase = (props) => {
};
}
function handleResetZoom() {
const [imgWidth, imgHeight] = getImgWidthHeight(
state.imageWidth,
state.imageHeight
);
dispatch(
createAction(ACTION_TYPES.update, {
width: imgWidth,
height: imgHeight,
scaleX: 1,
scaleY: 1,
top: state.top,
left: state.left,
loading: false,
percent: 100,
withTransition: true,
})
);
}
function handleZoom(targetX, targetY, direct, scale) {
let imgCenterXY = getImageCenterXY();
let diffX = targetX - imgCenterXY.x;
@ -542,8 +563,8 @@ const ViewerBase = (props) => {
dispatch(
createAction(ACTION_TYPES.update, {
width: width,
scaleX: scaleX,
scaleY: scaleY,
scaleX: scaleX > 0 ? scaleX : 0,
scaleY: scaleY > 0 ? scaleY : 0,
height: height,
top: top,
left: left,
@ -552,6 +573,8 @@ const ViewerBase = (props) => {
withTransition: true,
})
);
return [scaleX, scaleY];
}
let currentTop = (containerSize.current.height - state.height) / 2;
@ -618,6 +641,7 @@ const ViewerBase = (props) => {
opacity={state.opacity}
getImageCenterXY={getImageCenterXY}
handleZoom={handleZoom}
handleResetZoom={handleResetZoom}
height={state.height}
onNextClick={onNextClick}
onPrevClick={onPrevClick}

View File

@ -2,6 +2,7 @@ import * as React from "react";
import classnames from "classnames";
import ViewerLoading from "./viewer-loading";
import { useSwipeable } from "../../react-swipeable";
import { isMobile } from "react-device-detect";
export default function ViewerImage(props) {
const {
@ -12,10 +13,18 @@ export default function ViewerImage(props) {
playlistPos,
containerSize,
} = props;
const navMenuHeight = 53;
const isMouseDown = React.useRef(false);
const isZoomingRef = React.useRef(true);
const imgRef = React.useRef(null);
const unMountedRef = React.useRef(false);
React.useEffect(() => {
unMountedRef.current = false;
return () => (unMountedRef.current = true);
}, []);
const prePosition = React.useRef({
x: 0,
y: 0,
@ -25,8 +34,73 @@ export default function ViewerImage(props) {
y: 0,
});
const CompareTo = (a, b) => {
return Math.trunc(a) > Math.trunc(b);
};
const maybeAdjustImage = (point) => {
const imageBounds = imgRef.current.getBoundingClientRect();
const containerBounds = imgRef.current.parentNode.getBoundingClientRect();
const originalWidth = imgRef.current.clientWidth;
const widthOverhang = (imageBounds.width - originalWidth) / 2;
const originalHeight = imgRef.current.clientHeight;
const heightOverhang = (imageBounds.height - originalHeight) / 2;
const isWidthOutContainer = imageBounds.width >= containerBounds.width;
const isHeightOutContainer = imageBounds.height >= containerBounds.height;
if (
CompareTo(imageBounds.left, containerBounds.left) &&
isWidthOutContainer
) {
point.x = widthOverhang;
} else if (
CompareTo(containerBounds.right, imageBounds.right) &&
isWidthOutContainer
) {
point.x = -(imageBounds.width - containerBounds.width) + widthOverhang;
} else if (!isWidthOutContainer) {
point.x = (containerBounds.width - imageBounds.width) / 2 + widthOverhang;
}
if (
CompareTo(imageBounds.top, containerBounds.top) &&
isHeightOutContainer
) {
point.y = heightOverhang + navMenuHeight / 2;
} else if (
CompareTo(
containerBounds.bottom,
imageBounds.bottom + navMenuHeight / 2
) &&
isHeightOutContainer
) {
point.y =
-(imageBounds.height - containerBounds.height - navMenuHeight / 2) +
heightOverhang;
} else if (!isHeightOutContainer) {
point.y =
(containerBounds.height - imageBounds.height) / 2 +
heightOverhang +
navMenuHeight / 2;
}
return point;
};
const handlers = useSwipeable({
onSwiping: (e) => {
if (
e.piching ||
!isZoomingRef.current ||
unMountedRef.current ||
!imgRef.current
)
return;
const opacity =
props.scaleX !== 1 && props.scaleY !== 1
? 1
@ -35,66 +109,137 @@ export default function ViewerImage(props) {
const direction =
Math.abs(e.deltaX) > Math.abs(e.deltaY) ? "horizontal" : "vertical";
let swipeLeft = 0;
let Point = {
x: props.left + (e.deltaX * props.scaleX) / 15,
y: props.top + (e.deltaY * props.scaleY) / 15,
};
const isEdgeImage =
(playlistPos === 0 && e.deltaX > 0) ||
(playlistPos === playlist.length - 1 && e.deltaX < 0);
const newPoint = maybeAdjustImage(Point);
if (props.width < window.innerWidth) {
swipeLeft =
direction === "horizontal"
? isEdgeImage
? props.left
: e.deltaX > 0
? props.left + 2
: props.left - 2
: props.left;
} else {
swipeLeft =
direction === "horizontal" ? (isEdgeImage ? 0 : e.deltaX) : 0;
}
console.log(newPoint);
// let swipeLeft = 0;
// const isEdgeImage =
// (playlistPos === 0 && e.deltaX > 0) ||
// (playlistPos === playlist.length - 1 && e.deltaX < 0);
// if (props.width < window.innerWidth) {
// swipeLeft =
// direction === "horizontal"
// ? isEdgeImage
// ? props.left
// : e.deltaX > 0
// ? props.left + 2
// : props.left - 2
// : props.left;
// } else {
// swipeLeft =
// direction === "horizontal" ? (isEdgeImage ? 0 : e.deltaX) : 0;
// }
return dispatch(
createAction(actionType.update, {
left: swipeLeft,
left: newPoint.x,
top: newPoint.y,
opacity: direction === "vertical" && e.deltaY > 0 ? opacity : 1,
top:
direction === "vertical"
? e.deltaY >= 0
? props.currentTop + e.deltaY
: props.currentTop
: props.currentTop,
deltaY: direction === "vertical" ? (e.deltaY > 0 ? e.deltaY : 0) : 0,
deltaX: direction === "horizontal" ? e.deltaX : 0,
deltaX: 0,
deltaY: 0,
})
);
},
onSwipedLeft: (e) => {
if (props.scaleX !== 1 && props.scaleY !== 1) return;
if (
(props.scaleX !== 1 && props.scaleY !== 1) ||
e.piching ||
!isZoomingRef.current
)
return;
if (e.deltaX <= -100) props.onNextClick();
},
onSwipedRight: (e) => {
if (props.scaleX !== 1 && props.scaleY !== 1) return;
if (
(props.scaleX !== 1 && props.scaleY !== 1) ||
e.piching ||
!isZoomingRef.current
)
return;
if (e.deltaX >= 100) props.onPrevClick();
},
onSwipedDown: (e) => {
if (props.scaleX !== 1 && props.scaleY !== 1) return;
// if (unMountedRef.current) return;
// console.log("onSwiped");
// let Point = {
// x: props.left + (e.deltaX * props.scaleX) / 15,
// y: props.top + (e.deltaY * props.scaleY) / 15,
// };
// const newPoint = maybeAdjustImage(props.scaleX, props.scaleY, Point);
// return dispatch(
// createAction(actionType.update, {
// left: newPoint.x,
// top: newPoint.y,
// deltaX: 0,
// deltaY: 0,
// opacity: 1,
// })
// );
},
onZoom: (event) => {
if (unMountedRef.current || !imgRef.current) return;
const { handleZoom, handleResetZoom } = props;
const { scale, middleSegment } = event;
const zoomCondition = scale > 1;
const direct = zoomCondition ? 1 : -1;
const zoom = Math.abs(1 - scale) * 50;
if (zoom < 0.25) return;
if (isZoomingRef.current) {
isZoomingRef.current = false;
const [scaleX] = handleZoom(
middleSegment.x,
middleSegment.y,
direct,
zoom
);
setTimeout(() => {
if (scaleX < 1) handleResetZoom();
isZoomingRef.current = true;
}, 200);
}
},
onTouchEndOrOnMouseUp: (e) => {
if (
(props.scaleX !== 1 && props.scaleY !== 1) ||
e.piching ||
!isZoomingRef.current
)
return;
if (e.deltaY > 70) props.onMaskClick();
},
onSwiped: (e) => {
if (Math.abs(e.deltaX) < 100) {
const initialLeft = (containerSize.current.width - props.width) / 2;
onSwiped: () => {
console.log("onTouchEndOrOnMouseUp");
let Point = {
x: props.left,
y: props.top,
};
setTimeout(() => {
if (unMountedRef.current) return;
const newPoint = maybeAdjustImage(Point);
return dispatch(
createAction(actionType.update, {
left: props.width < window.innerWidth ? initialLeft : 0,
top: props.currentTop,
deltaY: 0,
left: newPoint.x,
top: newPoint.y,
deltaX: 0,
deltaY: 0,
opacity: 1,
})
);
}
}, 200);
},
});
@ -219,12 +364,30 @@ translateX(${props.left !== null ? props.left + "px" : "auto"}) translateY(${
let styleIndex = {
zIndex: props.zIndex,
top: isMobile ? navMenuHeight : 0,
};
let imgNode = null;
if (props.imgSrc !== "") {
imgNode = (
imgNode = isMobile ? (
<img
className={imgClass}
src={props.imgSrc}
ref={imgRef}
style={{
position: "absolute",
width: `${props.width}px`,
height: `${props.height}px`,
opacity: `${props.opacity}`,
transition: "all .5s ease-out",
top: props.top - navMenuHeight / 2,
left: props.left,
transform: `rotate(${props.rotate}deg) scaleX(${props.scaleX}) scaleY(${props.scaleY})`,
willChange: "transform",
}}
/>
) : (
<img
className={imgClass}
src={props.imgSrc}