Merge branch 'feature/new-viewer' of github.com:ONLYOFFICE/DocSpace into feature/new-viewer
This commit is contained in:
commit
15fd6c83f7
@ -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]
|
||||
|
@ -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 = (
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user