parent
6009b0595e
commit
94d0ba65c3
|
@ -4,6 +4,9 @@
|
|||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
// * zoom part from WebZ
|
||||
// * https://github.com/Ajaxy/telegram-tt/blob/069f4f5b2f2c7c22529ccced876842e7f9cb81f4/src/components/mediaViewer/MediaViewerSlides.tsx
|
||||
|
||||
import type {MyDocument} from '../lib/appManagers/appDocsManager';
|
||||
import type {MyPhoto} from '../lib/appManagers/appPhotosManager';
|
||||
import deferredPromise from '../helpers/cancellablePromise';
|
||||
|
@ -20,10 +23,10 @@ import ButtonIcon from './buttonIcon';
|
|||
import {ButtonMenuItemOptions} from './buttonMenu';
|
||||
import ButtonMenuToggle from './buttonMenuToggle';
|
||||
import ProgressivePreloader from './preloader';
|
||||
import SwipeHandler from './swipeHandler';
|
||||
import SwipeHandler, {ZoomDetails} from './swipeHandler';
|
||||
import {formatFullSentTime} from '../helpers/date';
|
||||
import appNavigationController, {NavigationItem} from './appNavigationController';
|
||||
import {Message} from '../layer';
|
||||
import {Message, PhotoSize} from '../layer';
|
||||
import findUpClassName from '../helpers/dom/findUpClassName';
|
||||
import renderImageFromUrl, {renderImageFromUrlPromise} from '../helpers/dom/renderImageFromUrl';
|
||||
import getVisibleRect from '../helpers/dom/getVisibleRect';
|
||||
|
@ -51,18 +54,23 @@ import overlayCounter from '../helpers/overlayCounter';
|
|||
import appDownloadManager from '../lib/appManagers/appDownloadManager';
|
||||
import wrapPeerTitle from './wrappers/peerTitle';
|
||||
import {toastNew} from './toast';
|
||||
import clamp from '../helpers/number/clamp';
|
||||
import debounce from '../helpers/schedulers/debounce';
|
||||
import isBetween from '../helpers/number/isBetween';
|
||||
|
||||
const ZOOM_STEP = 0.5;
|
||||
const ZOOM_INITIAL_VALUE = 1;
|
||||
const ZOOM_MIN_VALUE = 0.5;
|
||||
const ZOOM_MAX_VALUE = 4;
|
||||
|
||||
// TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию
|
||||
// TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода)
|
||||
// TODO: видео в мобильной вёрстке, если показываются элементы управления: если свайпнуть в сторону, то элементы вернутся на место, т.е. прыгнут - это не ок, надо бы замаскировать
|
||||
|
||||
export const MEDIA_VIEWER_CLASSNAME = 'media-viewer';
|
||||
|
||||
type Transform = {
|
||||
x: number;
|
||||
y: number;
|
||||
scale: number;
|
||||
};
|
||||
|
||||
export default class AppMediaViewerBase<
|
||||
ContentAdditionType extends string,
|
||||
ButtonsAdditionType extends string,
|
||||
|
@ -83,15 +91,10 @@ export default class AppMediaViewerBase<
|
|||
protected preloader: ProgressivePreloader = null;
|
||||
protected preloaderStreamable: ProgressivePreloader = null;
|
||||
|
||||
// protected targetContainer: HTMLElement = null;
|
||||
// protected loadMore: () => void = null;
|
||||
|
||||
protected log: ReturnType<typeof logger>;
|
||||
|
||||
protected isFirstOpen = true;
|
||||
|
||||
// protected needLoadMore = true;
|
||||
|
||||
protected pageEl = document.getElementById('page-chats') as HTMLDivElement;
|
||||
|
||||
protected setMoverPromise: Promise<void>;
|
||||
|
@ -113,18 +116,27 @@ export default class AppMediaViewerBase<
|
|||
btnIn: HTMLElement,
|
||||
rangeSelector: RangeSelector
|
||||
} = {} as any;
|
||||
// protected zoomValue = ZOOM_INITIAL_VALUE;
|
||||
protected zoomSwipeHandler: SwipeHandler;
|
||||
protected zoomSwipeStartX = 0;
|
||||
protected zoomSwipeStartY = 0;
|
||||
protected zoomSwipeX = 0;
|
||||
protected zoomSwipeY = 0;
|
||||
protected transform: Transform = {x: 0, y: 0, scale: ZOOM_INITIAL_VALUE};
|
||||
protected isZooming: boolean;
|
||||
protected isGesturingNow: boolean;
|
||||
protected isZoomingNow: boolean;
|
||||
protected draggingType: 'wheel' | 'touchmove' | 'mousemove';
|
||||
protected initialContentRect: DOMRect;
|
||||
|
||||
protected ctrlKeyDown: boolean;
|
||||
protected releaseSingleMedia: ReturnType<AppMediaPlaybackController['setSingleMedia']>;
|
||||
protected navigationItem: NavigationItem;
|
||||
|
||||
protected managers: AppManagers;
|
||||
protected swipeHandler: SwipeHandler;
|
||||
protected closing: boolean;
|
||||
|
||||
protected lastTransform: Transform = this.transform;
|
||||
protected lastZoomCenter: {x: number, y: number} = this.transform;
|
||||
protected lastDragOffset: {x: number, y: number} = this.transform;
|
||||
protected lastDragDelta: {x: number, y: number} = this.transform;
|
||||
protected lastGestureTime: number;
|
||||
protected clampZoomDebounced: ReturnType<typeof debounce<() => void>>;
|
||||
|
||||
get target() {
|
||||
return this.listLoader.current;
|
||||
|
@ -204,12 +216,12 @@ export default class AppMediaViewerBase<
|
|||
this.zoomElements.container.classList.add('zoom-container');
|
||||
|
||||
this.zoomElements.btnOut = ButtonIcon('zoomout', {noRipple: true});
|
||||
attachClickEvent(this.zoomElements.btnOut, () => this.changeZoom(false));
|
||||
attachClickEvent(this.zoomElements.btnOut, () => this.addZoomStep(false));
|
||||
this.zoomElements.btnIn = ButtonIcon('zoomin', {noRipple: true});
|
||||
attachClickEvent(this.zoomElements.btnIn, () => this.changeZoom(true));
|
||||
attachClickEvent(this.zoomElements.btnIn, () => this.addZoomStep(true));
|
||||
|
||||
this.zoomElements.rangeSelector = new RangeSelector({
|
||||
step: ZOOM_STEP,
|
||||
step: 0.01,
|
||||
min: ZOOM_MIN_VALUE,
|
||||
max: ZOOM_MAX_VALUE,
|
||||
withTransition: true
|
||||
|
@ -222,7 +234,9 @@ export default class AppMediaViewerBase<
|
|||
|
||||
this.zoomElements.container.append(this.zoomElements.btnOut, this.zoomElements.rangeSelector.container, this.zoomElements.btnIn);
|
||||
|
||||
this.wholeDiv.append(this.zoomElements.container);
|
||||
if(!IS_TOUCH_SUPPORTED && false) {
|
||||
this.wholeDiv.append(this.zoomElements.container);
|
||||
}
|
||||
|
||||
// * content
|
||||
this.content.main = document.createElement('div');
|
||||
|
@ -286,9 +300,9 @@ export default class AppMediaViewerBase<
|
|||
});
|
||||
|
||||
attachClickEvent(this.buttons.zoom, () => {
|
||||
if(this.isZooming()) this.toggleZoom(false);
|
||||
if(this.isZooming) this.resetZoom();
|
||||
else {
|
||||
this.changeZoom(true);
|
||||
this.addZoomStep(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -301,117 +315,350 @@ export default class AppMediaViewerBase<
|
|||
else this.onPrevClick(item);
|
||||
};
|
||||
|
||||
if(IS_TOUCH_SUPPORTED) {
|
||||
const swipeHandler = new SwipeHandler({
|
||||
element: this.wholeDiv,
|
||||
onSwipe: (xDiff, yDiff) => {
|
||||
if(isFullScreen()) {
|
||||
return;
|
||||
}
|
||||
const adjustPosition = (xDiff: number, yDiff: number) => {
|
||||
const [x, y] = [xDiff - this.lastDragOffset.x, yDiff - this.lastDragOffset.y];
|
||||
const [transform, inBoundsX, inBoundsY] = this.calculateOffsetBoundaries({
|
||||
x: this.transform.x + x,
|
||||
y: this.transform.y + y,
|
||||
scale: this.transform.scale
|
||||
});
|
||||
|
||||
const percents = Math.abs(xDiff) / windowSize.width;
|
||||
if(percents > .2 || xDiff > 125) {
|
||||
// console.log('will swipe', xDiff);
|
||||
this.lastDragDelta = {
|
||||
x,
|
||||
y
|
||||
};
|
||||
|
||||
if(xDiff > 0) {
|
||||
this.buttons.prev.click();
|
||||
} else {
|
||||
this.buttons.next.click();
|
||||
}
|
||||
this.lastDragOffset = {
|
||||
x: xDiff,
|
||||
y: yDiff
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
this.setTransform(transform);
|
||||
|
||||
const percentsY = Math.abs(yDiff) / windowSize.height;
|
||||
if(percentsY > .2 || yDiff > 125) {
|
||||
this.close();
|
||||
return true;
|
||||
}
|
||||
return {inBoundsX, inBoundsY};
|
||||
};
|
||||
|
||||
return false;
|
||||
},
|
||||
verifyTouchTarget: (e) => {
|
||||
// * Fix for seek input
|
||||
if(findUpClassName(e.target, 'ckin__controls') ||
|
||||
findUpClassName(e.target, 'media-viewer-caption') ||
|
||||
findUpClassName(e.target, 'media-viewer-topbar')) {
|
||||
return false;
|
||||
const setLastGestureTime = debounce(() => {
|
||||
this.lastGestureTime = Date.now();
|
||||
}, 500, false, true);
|
||||
|
||||
this.clampZoomDebounced = debounce(() => {
|
||||
this.onSwipeReset();
|
||||
}, 300, false, true);
|
||||
|
||||
this.swipeHandler = new SwipeHandler({
|
||||
element: this.wholeDiv,
|
||||
onReset: this.onSwipeReset,
|
||||
onFirstSwipe: this.onSwipeFirst,
|
||||
onSwipe: (xDiff, yDiff, e, cancelDrag) => {
|
||||
if(isFullScreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.isZooming && !this.isZoomingNow) {
|
||||
setLastGestureTime();
|
||||
|
||||
this.draggingType = e.type as any;
|
||||
const {inBoundsX, inBoundsY} = adjustPosition(xDiff, yDiff);
|
||||
cancelDrag?.(!inBoundsX, !inBoundsY);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.isZoomingNow || !IS_TOUCH_SUPPORTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const percents = Math.abs(xDiff) / windowSize.width;
|
||||
if(percents > .2 || Math.abs(xDiff) > 125) {
|
||||
if(xDiff > 0) {
|
||||
this.buttons.prev.click();
|
||||
} else {
|
||||
this.buttons.next.click();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const percentsY = Math.abs(yDiff) / windowSize.height;
|
||||
if(percentsY > .2 || Math.abs(yDiff) > 125) {
|
||||
this.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
onZoom: this.onZoom,
|
||||
onDoubleClick: ({centerX, centerY}) => {
|
||||
if(this.isZooming) {
|
||||
this.resetZoom();
|
||||
} else {
|
||||
const scale = ZOOM_INITIAL_VALUE + 2;
|
||||
this.changeZoomByPosition(centerX, centerY, scale);
|
||||
}
|
||||
},
|
||||
verifyTouchTarget: (e) => {
|
||||
// * Fix for seek input
|
||||
if(findUpClassName(e.target, 'ckin__controls') ||
|
||||
findUpClassName(e.target, 'media-viewer-caption') ||
|
||||
(findUpClassName(e.target, 'media-viewer-topbar') && e.type !== 'wheel')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
cursor: ''
|
||||
// cursor: 'move'
|
||||
});
|
||||
}
|
||||
|
||||
protected onSwipeFirst = () => {
|
||||
this.lastDragOffset = this.lastDragDelta = {x: 0, y: 0};
|
||||
this.lastTransform = {...this.transform};
|
||||
this.moversContainer.classList.add('no-transition');
|
||||
this.isGesturingNow = true;
|
||||
this.lastGestureTime = Date.now();
|
||||
this.clampZoomDebounced.clearTimeout();
|
||||
|
||||
if(!this.lastTransform.x && !this.lastTransform.y && !this.isZooming) {
|
||||
this.initialContentRect = this.content.media.getBoundingClientRect();
|
||||
}
|
||||
};
|
||||
|
||||
protected onSwipeReset = () => {
|
||||
// move
|
||||
this.moversContainer.classList.remove('no-transition');
|
||||
this.clampZoomDebounced.clearTimeout();
|
||||
|
||||
const {draggingType} = this;
|
||||
this.isZoomingNow = false;
|
||||
this.isGesturingNow = false;
|
||||
this.draggingType = undefined;
|
||||
|
||||
if(this.closing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.transform.scale > ZOOM_INITIAL_VALUE) {
|
||||
// Get current content boundaries
|
||||
const s1 = Math.min(this.transform.scale, ZOOM_MAX_VALUE);
|
||||
const scaleFactor = s1 / this.transform.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 = this.transform.x * scaleFactor + (this.lastZoomCenter.x - scaleFactor * this.lastZoomCenter.x);
|
||||
let y1 = this.transform.y * scaleFactor + (this.lastZoomCenter.y - scaleFactor * this.lastZoomCenter.y);
|
||||
|
||||
// If scale didn't change, we need to add inertia to pan gesture
|
||||
if(draggingType && draggingType !== 'wheel' && this.lastTransform.scale === this.transform.scale) {
|
||||
// Arbitrary pan velocity coefficient
|
||||
const k = 0.1;
|
||||
|
||||
// Calculate user gesture velocity
|
||||
const elapsedTime = Math.max(1, Date.now() - this.lastGestureTime);
|
||||
const Vx = Math.abs(this.lastDragOffset.x) / elapsedTime;
|
||||
const Vy = Math.abs(this.lastDragOffset.y) / elapsedTime;
|
||||
|
||||
// Add extra distance based on gesture velocity and last pan delta
|
||||
x1 -= Math.abs(this.lastDragOffset.x) * Vx * k * -this.lastDragDelta.x;
|
||||
y1 -= Math.abs(this.lastDragOffset.y) * Vy * k * -this.lastDragDelta.y;
|
||||
}
|
||||
|
||||
const [transform] = this.calculateOffsetBoundaries({x: x1, y: y1, scale: s1});
|
||||
this.lastTransform = transform;
|
||||
this.setTransform(transform);
|
||||
} else if(this.transform.scale < ZOOM_INITIAL_VALUE) {
|
||||
this.resetZoom();
|
||||
}
|
||||
};
|
||||
|
||||
protected onZoom = ({
|
||||
initialCenterX,
|
||||
initialCenterY,
|
||||
zoom,
|
||||
zoomAdd,
|
||||
currentCenterX,
|
||||
currentCenterY,
|
||||
dragOffsetX,
|
||||
dragOffsetY,
|
||||
zoomFactor
|
||||
}: ZoomDetails) => {
|
||||
initialCenterX ||= windowSize.width / 2;
|
||||
initialCenterY ||= windowSize.height / 2;
|
||||
currentCenterX ||= windowSize.width / 2;
|
||||
currentCenterY ||= windowSize.height / 2;
|
||||
|
||||
this.isZoomingNow = true;
|
||||
|
||||
const zoomMaxBounceValue = ZOOM_MAX_VALUE * 3;
|
||||
const scale = zoomAdd ? clamp(this.lastTransform.scale + zoomAdd, ZOOM_MIN_VALUE, zoomMaxBounceValue) : (zoom ?? clamp(this.lastTransform.scale * zoomFactor, ZOOM_MIN_VALUE, zoomMaxBounceValue));
|
||||
const scaleFactor = scale / this.lastTransform.scale;
|
||||
const offsetX = Math.abs(Math.min(this.lastTransform.x, 0));
|
||||
const offsetY = Math.abs(Math.min(this.lastTransform.y, 0));
|
||||
|
||||
// Save last zoom center for bounce back effect
|
||||
this.lastZoomCenter = {
|
||||
x: currentCenterX,
|
||||
y: currentCenterY
|
||||
};
|
||||
|
||||
// Calculate new center relative to the shifted image
|
||||
const scaledCenterX = offsetX + initialCenterX;
|
||||
const scaledCenterY = offsetY + initialCenterY;
|
||||
|
||||
const {scaleOffsetX, scaleOffsetY} = this.calculateScaleOffset({x: scaledCenterX, y: scaledCenterY, scale: scaleFactor});
|
||||
|
||||
const [transform] = this.calculateOffsetBoundaries({
|
||||
x: this.lastTransform.x + scaleOffsetX + dragOffsetX,
|
||||
y: this.lastTransform.y + scaleOffsetY + dragOffsetY,
|
||||
scale
|
||||
});
|
||||
|
||||
this.setTransform(transform);
|
||||
};
|
||||
|
||||
protected changeZoomByPosition(x: number, y: number, scale: number) {
|
||||
const {scaleOffsetX, scaleOffsetY} = this.calculateScaleOffset({x, y, scale});
|
||||
const transform = this.calculateOffsetBoundaries({
|
||||
x: scaleOffsetX,
|
||||
y: scaleOffsetY,
|
||||
scale
|
||||
})[0];
|
||||
|
||||
this.setTransform(transform);
|
||||
}
|
||||
|
||||
protected setTransform(transform: Transform) {
|
||||
this.transform = transform;
|
||||
this.changeZoom(transform.scale);
|
||||
}
|
||||
|
||||
// Calculate how much we need to shift the image to keep the zoom center at the same position
|
||||
protected calculateScaleOffset({x, y, scale}: {
|
||||
x: number,
|
||||
y: number,
|
||||
scale: number
|
||||
}) {
|
||||
return {
|
||||
scaleOffsetX: x - scale * x,
|
||||
scaleOffsetY: y - scale * y
|
||||
};
|
||||
}
|
||||
|
||||
protected toggleZoom(enable?: boolean) {
|
||||
const isVisible = this.isZooming();
|
||||
const isVisible = this.isZooming;
|
||||
const auto = enable === undefined;
|
||||
if(this.zoomElements.rangeSelector.mousedown || this.ctrlKeyDown) {
|
||||
enable = true;
|
||||
}
|
||||
|
||||
if(isVisible === enable) return;
|
||||
enable ??= !isVisible;
|
||||
|
||||
if(enable === undefined) {
|
||||
enable = !isVisible;
|
||||
if(isVisible === enable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buttons.zoom.classList.toggle('zoom-in', !enable);
|
||||
this.zoomElements.container.classList.toggle('is-visible', enable);
|
||||
const zoomValue = enable ? this.zoomElements.rangeSelector.value : 1;
|
||||
this.setZoomValue(zoomValue);
|
||||
this.zoomElements.rangeSelector.setProgress(zoomValue);
|
||||
this.zoomElements.container.classList.toggle('is-visible', this.isZooming = enable);
|
||||
this.wholeDiv.classList.toggle('is-zooming', enable);
|
||||
|
||||
if(auto || !enable) {
|
||||
const zoomValue = enable ? this.transform.scale : ZOOM_INITIAL_VALUE;
|
||||
this.setZoomValue(zoomValue);
|
||||
this.zoomElements.rangeSelector.setProgress(zoomValue);
|
||||
}
|
||||
|
||||
if(this.videoPlayer) {
|
||||
this.videoPlayer.lockControls(enable ? false : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
if(enable) {
|
||||
if(!this.zoomSwipeHandler) {
|
||||
let lastDiffX: number, lastDiffY: number;
|
||||
const multiplier = 1;
|
||||
this.zoomSwipeHandler = new SwipeHandler({
|
||||
element: this.moversContainer,
|
||||
onFirstSwipe: () => {
|
||||
lastDiffX = lastDiffY = 0;
|
||||
this.moversContainer.classList.add('no-transition');
|
||||
},
|
||||
onSwipe: (xDiff, yDiff) => {
|
||||
[xDiff, yDiff] = [xDiff * multiplier, yDiff * multiplier];
|
||||
this.zoomSwipeX += xDiff - lastDiffX;
|
||||
this.zoomSwipeY += yDiff - lastDiffY;
|
||||
[lastDiffX, lastDiffY] = [xDiff, yDiff];
|
||||
protected addZoomStep(add: boolean) {
|
||||
this.addZoom(ZOOM_STEP * (add ? 1 : -1));
|
||||
}
|
||||
|
||||
this.setZoomValue();
|
||||
},
|
||||
onReset: () => {
|
||||
this.moversContainer.classList.remove('no-transition');
|
||||
},
|
||||
cursor: 'move'
|
||||
});
|
||||
} else {
|
||||
this.zoomSwipeHandler.setListeners();
|
||||
}
|
||||
protected resetZoom() {
|
||||
this.setTransform({
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: ZOOM_INITIAL_VALUE
|
||||
});
|
||||
}
|
||||
|
||||
this.zoomElements.rangeSelector.setProgress(zoomValue);
|
||||
} else if(!enable) {
|
||||
this.zoomSwipeHandler.removeListeners();
|
||||
protected changeZoom(value = this.transform.scale) {
|
||||
this.transform.scale = value;
|
||||
this.zoomElements.rangeSelector.setProgress(value);
|
||||
this.setZoomValue(value);
|
||||
}
|
||||
|
||||
protected addZoom(value: number) {
|
||||
this.lastTransform = this.transform;
|
||||
this.onZoom({
|
||||
zoomAdd: value,
|
||||
currentCenterX: 0,
|
||||
currentCenterY: 0,
|
||||
initialCenterX: 0,
|
||||
initialCenterY: 0,
|
||||
dragOffsetX: 0,
|
||||
dragOffsetY: 0
|
||||
});
|
||||
this.lastTransform = this.transform;
|
||||
this.clampZoomDebounced();
|
||||
}
|
||||
|
||||
protected getZoomBounce() {
|
||||
return this.isGesturingNow && IS_TOUCH_SUPPORTED ? 50 : 0;
|
||||
}
|
||||
|
||||
protected calculateOffsetBoundaries = (
|
||||
{x, y, scale}: Transform,
|
||||
offsetTop = 0
|
||||
): [Transform, boolean, boolean] => {
|
||||
if(!this.initialContentRect) return [{x, y, scale}, true, true];
|
||||
// Get current content boundaries
|
||||
let inBoundsX = true;
|
||||
let inBoundsY = true;
|
||||
|
||||
const {minX, maxX, minY, maxY} = this.getZoomBoundaries(scale, offsetTop);
|
||||
|
||||
inBoundsX = isBetween(x, maxX, minX);
|
||||
x = clamp(x, maxX, minX);
|
||||
|
||||
inBoundsY = isBetween(y, maxY, minY);
|
||||
y = clamp(y, maxY, minY);
|
||||
|
||||
return [{x, y, scale}, inBoundsX, inBoundsY];
|
||||
};
|
||||
|
||||
protected getZoomBoundaries(scale = this.transform.scale, offsetTop = 0) {
|
||||
if(!this.initialContentRect) {
|
||||
return {minX: 0, maxX: 0, minY: 0, maxY: 0};
|
||||
}
|
||||
|
||||
const centerX = (windowSize.width - windowSize.width * scale) / 2;
|
||||
const centerY = (windowSize.height - windowSize.height * scale) / 2;
|
||||
|
||||
// If content is outside window we calculate offset boundaries
|
||||
// based on initial content rect and current scale
|
||||
const minX = Math.max(-this.initialContentRect.left * scale, centerX);
|
||||
const maxX = windowSize.width - this.initialContentRect.right * scale;
|
||||
|
||||
const minY = Math.max(-this.initialContentRect.top * scale + offsetTop, centerY);
|
||||
const maxY = windowSize.height - this.initialContentRect.bottom * scale;
|
||||
|
||||
return {minX, maxX, minY, maxY};
|
||||
}
|
||||
|
||||
protected changeZoom(add: boolean) {
|
||||
this.zoomElements.rangeSelector.addProgress(ZOOM_STEP * (add ? 1 : -1));
|
||||
this.setZoomValue();
|
||||
}
|
||||
protected setZoomValue = (value = this.transform.scale) => {
|
||||
this.initialContentRect ??= this.content.media.getBoundingClientRect();
|
||||
|
||||
protected setZoomValue = (value = this.zoomElements.rangeSelector.value) => {
|
||||
// this.zoomValue = value;
|
||||
if(value === ZOOM_INITIAL_VALUE) {
|
||||
this.zoomSwipeX = 0;
|
||||
this.zoomSwipeY = 0;
|
||||
this.transform.x = 0;
|
||||
this.transform.y = 0;
|
||||
}
|
||||
|
||||
this.moversContainer.style.transform = `matrix(${value}, 0, 0, ${value}, ${this.zoomSwipeX}, ${this.zoomSwipeY})`;
|
||||
this.moversContainer.style.transform = `translate3d(${this.transform.x.toFixed(3)}px, ${this.transform.y.toFixed(3)}px, 0px) scale(${value.toFixed(3)})`;
|
||||
|
||||
this.zoomElements.btnOut.classList.toggle('inactive', value === ZOOM_MIN_VALUE);
|
||||
this.zoomElements.btnIn.classList.toggle('inactive', value === ZOOM_MAX_VALUE);
|
||||
|
@ -419,10 +666,6 @@ export default class AppMediaViewerBase<
|
|||
this.toggleZoom(value !== ZOOM_INITIAL_VALUE);
|
||||
};
|
||||
|
||||
protected isZooming() {
|
||||
return this.zoomElements.container.classList.contains('is-visible');
|
||||
}
|
||||
|
||||
protected setBtnMenuToggle(buttons: ButtonMenuItemOptions[]) {
|
||||
const btnMenuToggle = ButtonMenuToggle({buttonOptions: {onlyMobile: true}, direction: 'bottom-left', buttons});
|
||||
this.topbar.append(btnMenuToggle);
|
||||
|
@ -435,6 +678,9 @@ export default class AppMediaViewerBase<
|
|||
|
||||
if(this.setMoverAnimationPromise) return Promise.reject();
|
||||
|
||||
this.closing = true;
|
||||
this.swipeHandler?.removeListeners();
|
||||
|
||||
if(this.navigationItem) {
|
||||
appNavigationController.removeItem(this.navigationItem);
|
||||
}
|
||||
|
@ -459,8 +705,6 @@ export default class AppMediaViewerBase<
|
|||
|
||||
this.removeGlobalListeners();
|
||||
|
||||
this.zoomSwipeHandler = undefined;
|
||||
|
||||
promise.finally(() => {
|
||||
this.wholeDiv.remove();
|
||||
this.toggleOverlay(false);
|
||||
|
@ -480,23 +724,13 @@ export default class AppMediaViewerBase<
|
|||
}
|
||||
|
||||
protected removeGlobalListeners() {
|
||||
if(this.zoomSwipeHandler) {
|
||||
this.zoomSwipeHandler.removeListeners();
|
||||
}
|
||||
|
||||
window.removeEventListener('keydown', this.onKeyDown);
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
window.removeEventListener('wheel', this.onWheel, {capture: true});
|
||||
}
|
||||
|
||||
protected setGlobalListeners() {
|
||||
if(this.isZooming()) {
|
||||
this.zoomSwipeHandler.setListeners();
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', this.onKeyDown);
|
||||
window.addEventListener('keyup', this.onKeyUp);
|
||||
if(!IS_TOUCH_SUPPORTED) window.addEventListener('wheel', this.onWheel, {passive: false, capture: true});
|
||||
}
|
||||
|
||||
onClick = (e: MouseEvent) => {
|
||||
|
@ -521,7 +755,7 @@ export default class AppMediaViewerBase<
|
|||
return;
|
||||
}
|
||||
|
||||
const isZooming = this.isZooming();
|
||||
const isZooming = this.isZooming;
|
||||
let mover: HTMLElement = null;
|
||||
const classNames = ['ckin__player', 'media-viewer-buttons', 'media-viewer-author', 'media-viewer-caption', 'zoom-container'];
|
||||
if(isZooming) {
|
||||
|
@ -550,12 +784,12 @@ export default class AppMediaViewerBase<
|
|||
|
||||
let good = true;
|
||||
if(key === 'ArrowRight') {
|
||||
this.buttons.next.click();
|
||||
!this.isZooming && this.buttons.next.click();
|
||||
} else if(key === 'ArrowLeft') {
|
||||
this.buttons.prev.click();
|
||||
!this.isZooming && this.buttons.prev.click();
|
||||
} else if(key === '-' || key === '=') {
|
||||
if(this.ctrlKeyDown) {
|
||||
this.changeZoom(key === '=');
|
||||
this.addZoomStep(key === '=');
|
||||
}
|
||||
} else {
|
||||
good = false;
|
||||
|
@ -578,26 +812,12 @@ export default class AppMediaViewerBase<
|
|||
if(!(e.ctrlKey || e.metaKey)) {
|
||||
this.ctrlKeyDown = false;
|
||||
|
||||
if(this.isZooming()) {
|
||||
if(this.isZooming) {
|
||||
this.setZoomValue();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onWheel = (e: WheelEvent) => {
|
||||
if(overlayCounter.overlaysActive > 1 || (findUpClassName(e.target, 'media-viewer-caption') && !this.ctrlKeyDown)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelEvent(e);
|
||||
|
||||
if(this.ctrlKeyDown) {
|
||||
const scrollingUp = e.deltaY < 0;
|
||||
// if(!scrollingUp && !this.isZooming()) return;
|
||||
this.changeZoom(!!scrollingUp);
|
||||
}
|
||||
};
|
||||
|
||||
protected async setMoverToTarget(target: HTMLElement, closing = false, fromRight = 0) {
|
||||
this.dispatchEvent('setMoverBefore');
|
||||
|
||||
|
@ -608,7 +828,7 @@ export default class AppMediaViewerBase<
|
|||
// mover.append(this.buttons.prev, this.buttons.next);
|
||||
}
|
||||
|
||||
const zoomValue = this.isZooming() && closing /* && false */ ? this.zoomElements.rangeSelector.value : ZOOM_INITIAL_VALUE;
|
||||
const zoomValue = this.isZooming && closing /* && false */ ? this.transform.scale : ZOOM_INITIAL_VALUE;
|
||||
/* if(!(zoomValue > 1 && closing)) */ this.removeCenterFromMover(mover);
|
||||
|
||||
const wasActive = fromRight !== 0;
|
||||
|
@ -749,12 +969,8 @@ export default class AppMediaViewerBase<
|
|||
// let borderRadius = '0px 0px 0px 0px';
|
||||
|
||||
if(closing && zoomValue !== 1) {
|
||||
// const width = this.moversContainer.scrollWidth * scaleX;
|
||||
// const height = this.moversContainer.scrollHeight * scaleY;
|
||||
const willBeLeft = windowSize.width / 2 - rect.width / 2;
|
||||
const willBeTop = windowSize.height / 2 - rect.height / 2;
|
||||
const left = rect.left - willBeLeft/* + (width - rect.width) / 2 */;
|
||||
const top = rect.top - willBeTop/* + (height - rect.height) / 2 */;
|
||||
const left = rect.left - (windowSize.width * scaleX - rect.width) / 2;
|
||||
const top = rect.top - (windowSize.height * scaleY - rect.height) / 2;
|
||||
this.moversContainer.style.transform = `matrix(${scaleX}, 0, 0, ${scaleY}, ${left}, ${top})`;
|
||||
} else {
|
||||
mover.style.transform = transform;
|
||||
|
@ -1288,6 +1504,23 @@ export default class AppMediaViewerBase<
|
|||
this.moveTheMover(this.content.mover, fromRight === 1);
|
||||
this.setNewMover();
|
||||
} else {
|
||||
this.navigationItem = {
|
||||
type: 'media',
|
||||
onPop: (canAnimate) => {
|
||||
if(this.setMoverAnimationPromise) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!canAnimate && IS_MOBILE_SAFARI) {
|
||||
this.wholeDiv.remove();
|
||||
}
|
||||
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
appNavigationController.pushItem(this.navigationItem);
|
||||
|
||||
this.toggleOverlay(true);
|
||||
this.setGlobalListeners();
|
||||
await setAuthorPromise;
|
||||
|
@ -1298,21 +1531,6 @@ export default class AppMediaViewerBase<
|
|||
}
|
||||
|
||||
this.toggleWholeActive(true);
|
||||
|
||||
if(!IS_MOBILE_SAFARI) {
|
||||
this.navigationItem = {
|
||||
type: 'media',
|
||||
onPop: (canAnimate) => {
|
||||
if(this.setMoverAnimationPromise) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
appNavigationController.pushItem(this.navigationItem);
|
||||
}
|
||||
}
|
||||
|
||||
// //////this.log('wasActive:', wasActive);
|
||||
|
@ -1358,8 +1576,8 @@ export default class AppMediaViewerBase<
|
|||
const supportsStreaming: boolean = !!(isDocument && media.supportsStreaming);
|
||||
const preloader = supportsStreaming ? this.preloaderStreamable : this.preloader;
|
||||
|
||||
const getCacheContext = () => {
|
||||
return this.managers.thumbsStorage.getCacheContext(media, size?.type);
|
||||
const getCacheContext = (type = size?.type) => {
|
||||
return this.managers.thumbsStorage.getCacheContext(media, type);
|
||||
};
|
||||
|
||||
let setMoverPromise: Promise<void>;
|
||||
|
@ -1496,7 +1714,7 @@ export default class AppMediaViewerBase<
|
|||
this.videoPlayer = undefined;
|
||||
}, {once: true});
|
||||
|
||||
if(this.isZooming()) {
|
||||
if(this.isZooming) {
|
||||
this.videoPlayer.lockControls(false);
|
||||
}
|
||||
/* div.append(video);
|
||||
|
@ -1630,6 +1848,10 @@ export default class AppMediaViewerBase<
|
|||
const load = async() => {
|
||||
const cancellablePromise = isDocument ? appDownloadManager.downloadMediaURL({media}) : appDownloadManager.downloadMediaURL({media, thumb: size});
|
||||
|
||||
const photoSizes = !isDocument && media.sizes.slice().filter((size) => (size as PhotoSize.photoSize).w) as PhotoSize.photoSize[];
|
||||
photoSizes?.sort((a, b) => b.size - a.size);
|
||||
const cancellableFullPromise = !isDocument && appDownloadManager.downloadMediaURL({media, thumb: photoSizes?.[0]});
|
||||
|
||||
onAnimationEnd.then(async() => {
|
||||
if(!(await getCacheContext()).url) {
|
||||
this.preloader.attachPromise(cancellablePromise);
|
||||
|
@ -1652,31 +1874,41 @@ export default class AppMediaViewerBase<
|
|||
|
||||
if(mediaSizes.isMobile) {
|
||||
const imgs = mover.querySelectorAll('img');
|
||||
if(imgs && imgs.length) {
|
||||
imgs.forEach((img) => {
|
||||
img.classList.remove('thumbnail'); // может здесь это вообще не нужно
|
||||
});
|
||||
}
|
||||
imgs.forEach((img) => {
|
||||
img.classList.remove('thumbnail'); // может здесь это вообще не нужно
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const div = mover.firstElementChild && mover.firstElementChild.classList.contains('media-viewer-aspecter') ? mover.firstElementChild : mover;
|
||||
const haveImage = div.firstElementChild?.tagName === 'IMG' ? div.firstElementChild as HTMLImageElement : null;
|
||||
if(!haveImage || haveImage.src !== url) {
|
||||
const haveImage = ['CANVAS', 'IMG'].includes(div.firstElementChild?.tagName) ? div.firstElementChild as HTMLElement : null;
|
||||
if((haveImage as HTMLImageElement)?.src !== url) {
|
||||
const image = new Image();
|
||||
image.classList.add('thumbnail');
|
||||
|
||||
// this.log('will renderImageFromUrl:', image, div, target);
|
||||
|
||||
renderImageFromUrl(image, url, () => {
|
||||
this.updateMediaSource(target, url, 'img');
|
||||
fastRaf(() => {
|
||||
this.updateMediaSource(target, url, 'img');
|
||||
|
||||
if(haveImage) {
|
||||
if(haveImage) {
|
||||
fastRaf(() => {
|
||||
haveImage.remove();
|
||||
});
|
||||
}
|
||||
|
||||
div.append(image);
|
||||
});
|
||||
}, false);
|
||||
|
||||
cancellableFullPromise?.then((url) => {
|
||||
const fullImage = new Image();
|
||||
fullImage.classList.add('thumbnail');
|
||||
renderImageFromUrl(fullImage, url, () => {
|
||||
fastRaf(() => {
|
||||
haveImage.remove();
|
||||
image.replaceWith(fullImage);
|
||||
});
|
||||
}
|
||||
|
||||
div.append(image);
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
// * zoom part from WebZ
|
||||
// * https://github.com/Ajaxy/telegram-tt/blob/069f4f5b2f2c7c22529ccced876842e7f9cb81f4/src/util/captureEvents.ts
|
||||
|
||||
import cancelEvent from '../helpers/dom/cancelEvent';
|
||||
import IS_TOUCH_SUPPORTED from '../environment/touchSupport';
|
||||
import safeAssign from '../helpers/object/safeAssign';
|
||||
|
@ -13,12 +16,18 @@ import ListenerSetter, {Listener, ListenerOptions} from '../helpers/listenerSett
|
|||
import {attachContextMenuListener} from '../helpers/dom/attachContextMenuListener';
|
||||
import pause from '../helpers/schedulers/pause';
|
||||
import deferredPromise from '../helpers/cancellablePromise';
|
||||
import clamp from '../helpers/number/clamp';
|
||||
import debounce from '../helpers/schedulers/debounce';
|
||||
import {logger} from '../lib/logger';
|
||||
import isSwipingBackSafari from '../helpers/dom/isSwipingBackSafari';
|
||||
import windowSize from '../helpers/windowSize';
|
||||
|
||||
type E = {
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
target: EventTarget,
|
||||
button?: number
|
||||
button?: number,
|
||||
type?: string
|
||||
};
|
||||
|
||||
type EE = E | (Exclude<E, 'clientX' | 'clientY'> & {
|
||||
|
@ -29,6 +38,18 @@ const getEvent = (e: EE) => {
|
|||
return 'touches' in e ? e.touches[0] : e;
|
||||
};
|
||||
|
||||
function getDistance(a: Touch, b?: Touch) {
|
||||
if(!b) return 0;
|
||||
return Math.hypot((b.pageX - a.pageX), (b.pageY - a.pageY));
|
||||
}
|
||||
|
||||
function getTouchCenter(a: Touch, b: Touch) {
|
||||
return {
|
||||
x: (a.pageX + b.pageX) / 2,
|
||||
y: (a.pageY + b.pageY) / 2
|
||||
};
|
||||
}
|
||||
|
||||
const attachGlobalListenerTo = document;
|
||||
|
||||
let RESET_GLOBAL = false;
|
||||
|
@ -43,24 +64,45 @@ export type SwipeHandlerOptions = {
|
|||
onFirstSwipe?: SwipeHandler['onFirstSwipe'],
|
||||
onReset?: SwipeHandler['onReset'],
|
||||
onStart?: SwipeHandler['onStart'],
|
||||
onZoom?: SwipeHandler['onZoom'],
|
||||
onDrag?: SwipeHandler['onDrag'],
|
||||
onDoubleClick?: SwipeHandler['onDoubleClick'],
|
||||
cursor?: SwipeHandler['cursor'],
|
||||
cancelEvent?: SwipeHandler['cancelEvent'],
|
||||
listenerOptions?: SwipeHandler['listenerOptions'],
|
||||
setCursorTo?: HTMLElement,
|
||||
middleware?: Middleware,
|
||||
withDelay?: boolean
|
||||
withDelay?: boolean,
|
||||
minZoom?: number,
|
||||
maxZoom?: number
|
||||
};
|
||||
|
||||
const TOUCH_MOVE_OPTIONS: ListenerOptions = {passive: false};
|
||||
const MOUSE_MOVE_OPTIONS: ListenerOptions = false as any;
|
||||
const WHEEL_OPTIONS: ListenerOptions = {capture: true, passive: false};
|
||||
|
||||
export type ZoomDetails = {
|
||||
zoom?: number;
|
||||
zoomFactor?: number;
|
||||
zoomAdd?: number;
|
||||
initialCenterX: number;
|
||||
initialCenterY: number;
|
||||
dragOffsetX: number;
|
||||
dragOffsetY: number;
|
||||
currentCenterX: number;
|
||||
currentCenterY: number;
|
||||
};
|
||||
|
||||
export default class SwipeHandler {
|
||||
private element: HTMLElement;
|
||||
private onSwipe: (xDiff: number, yDiff: number, e: EE) => boolean | void;
|
||||
private onSwipe: (xDiff: number, yDiff: number, e: EE, cancelDrag?: (x: boolean, y: boolean) => void) => boolean | void;
|
||||
private verifyTouchTarget: (evt: EE) => boolean | Promise<boolean>;
|
||||
private onFirstSwipe: (e: EE) => void;
|
||||
private onReset: () => void;
|
||||
private onStart: () => void;
|
||||
private onZoom: (details: ZoomDetails) => void;
|
||||
private onDrag: (e: EE, captureEvent: E, details: {dragOffsetX: number, dragOffsetY: number}, cancelDrag: (x: boolean, y: boolean) => void) => void;
|
||||
private onDoubleClick: (details: {centerX: number, centerY: number}) => void;
|
||||
private cursor: 'grabbing' | 'move' | 'row-resize' | 'col-resize' | 'nesw-resize' | 'nwse-resize' | 'ne-resize' | 'se-resize' | 'sw-resize' | 'nw-resize' | 'n-resize' | 'e-resize' | 's-resize' | 'w-resize' | '';
|
||||
private cancelEvent: boolean;
|
||||
private listenerOptions: ListenerOptions;
|
||||
|
@ -79,9 +121,20 @@ export default class SwipeHandler {
|
|||
private withDelay: boolean;
|
||||
private listenerSetter: ListenerSetter;
|
||||
|
||||
private initialDistance: number;
|
||||
private initialTouchCenter: ReturnType<typeof getTouchCenter>;
|
||||
private initialDragOffset: {x: number, y: number};
|
||||
private isDragCanceled: {x: boolean, y: boolean};
|
||||
private wheelZoom: number;
|
||||
private releaseWheelDrag: ReturnType<typeof debounce<(e: Event) => void>>;
|
||||
private releaseWheelZoom: ReturnType<typeof debounce<(e: Event) => void>>;
|
||||
|
||||
private log: ReturnType<typeof logger>;
|
||||
|
||||
constructor(options: SwipeHandlerOptions) {
|
||||
safeAssign(this, options);
|
||||
|
||||
this.log = logger('SWIPE-HANDLER');
|
||||
this.cursor ??= 'grabbing';
|
||||
this.cancelEvent ??= true;
|
||||
// this.listenerOptions ??= false as any;
|
||||
|
@ -98,6 +151,9 @@ export default class SwipeHandler {
|
|||
this.reset();
|
||||
this.removeListeners();
|
||||
});
|
||||
|
||||
this.releaseWheelDrag = debounce(this.reset, 150, false);
|
||||
this.releaseWheelZoom = debounce(this.reset, 150, false);
|
||||
}
|
||||
|
||||
public setListeners() {
|
||||
|
@ -105,6 +161,10 @@ export default class SwipeHandler {
|
|||
// @ts-ignore
|
||||
this.listenerSetter.add(this.element)('mousedown', this.handleStart, this.listenerOptions);
|
||||
this.listenerSetter.add(attachGlobalListenerTo)('mouseup', this.reset);
|
||||
|
||||
if(this.onZoom || this.onDoubleClick) {
|
||||
this.listenerSetter.add(this.element)('wheel', this.handleWheel, WHEEL_OPTIONS);
|
||||
}
|
||||
} else {
|
||||
if(this.withDelay) {
|
||||
attachContextMenuListener({
|
||||
|
@ -122,11 +182,19 @@ export default class SwipeHandler {
|
|||
this.listenerSetter.add(this.element)('touchstart', this.handleStart, this.listenerOptions);
|
||||
}
|
||||
|
||||
if(this.onDoubleClick) {
|
||||
this.listenerSetter.add(this.element)('dblclick', (e) => {
|
||||
this.onDoubleClick({centerX: e.pageX, centerY: e.pageY});
|
||||
});
|
||||
}
|
||||
|
||||
this.listenerSetter.add(attachGlobalListenerTo)('touchend', this.reset);
|
||||
}
|
||||
}
|
||||
|
||||
public removeListeners() {
|
||||
this.log('remove listeners');
|
||||
this.reset();
|
||||
this.listenerSetter.removeAll();
|
||||
}
|
||||
|
||||
|
@ -157,9 +225,21 @@ export default class SwipeHandler {
|
|||
this.eventUp =
|
||||
this.isMouseDown =
|
||||
undefined;
|
||||
|
||||
if(this.onZoom) {
|
||||
this.initialDistance = 0;
|
||||
this.initialTouchCenter = {
|
||||
x: windowSize.width / 2,
|
||||
y: windowSize.height / 2
|
||||
};
|
||||
this.initialDragOffset = {x: 0, y: 0};
|
||||
this.isDragCanceled = {x: false, y: false};
|
||||
this.wheelZoom = 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected reset = (e?: Event) => {
|
||||
this.log('reset');
|
||||
/* if(e) {
|
||||
cancelEvent(e);
|
||||
} */
|
||||
|
@ -175,11 +255,38 @@ export default class SwipeHandler {
|
|||
this.onReset?.();
|
||||
}
|
||||
|
||||
this.releaseWheelDrag?.clearTimeout();
|
||||
this.releaseWheelZoom?.clearTimeout();
|
||||
|
||||
this.resetValues();
|
||||
};
|
||||
|
||||
protected setHadMove(_e: EE) {
|
||||
if(!this.hadMove) {
|
||||
this.log('had move');
|
||||
this.hadMove = true;
|
||||
this.setCursorTo.style.setProperty('cursor', this.cursor, 'important');
|
||||
this.onFirstSwipe?.(_e);
|
||||
}
|
||||
}
|
||||
|
||||
protected dispatchOnSwipe(...args: Parameters<SwipeHandlerOptions['onSwipe']>) {
|
||||
const onSwipeResult = this.onSwipe(...args);
|
||||
if(onSwipeResult !== undefined && onSwipeResult) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
protected handleStart = async(_e: EE) => {
|
||||
this.log('start');
|
||||
|
||||
if(this.isMouseDown) {
|
||||
const touches = (_e as any as TouchEvent).touches;
|
||||
if(touches?.length === 2) {
|
||||
this.initialDistance = getDistance(touches[0], touches[1]);
|
||||
this.initialTouchCenter = getTouchCenter(touches[0], touches[1]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -188,11 +295,31 @@ export default class SwipeHandler {
|
|||
return;
|
||||
}
|
||||
|
||||
if(this.verifyTouchTarget && !(await this.verifyTouchTarget(_e))) {
|
||||
return this.reset();
|
||||
if(isSwipingBackSafari(_e as any)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tempId = ++this.tempId;
|
||||
|
||||
const verifyResult = this.verifyTouchTarget?.(_e);
|
||||
if(verifyResult !== undefined) {
|
||||
let result: any;
|
||||
if(verifyResult instanceof Promise) {
|
||||
// const tempId = this.tempId;
|
||||
result = await verifyResult;
|
||||
|
||||
if(this.tempId !== tempId) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
result = verifyResult;
|
||||
}
|
||||
|
||||
if(!result) {
|
||||
return this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
this.isMouseDown = true;
|
||||
|
||||
if(this.withDelay && !IS_TOUCH_SUPPORTED) {
|
||||
|
@ -245,6 +372,12 @@ export default class SwipeHandler {
|
|||
cancelEvent(_e as any);
|
||||
}
|
||||
|
||||
if(this.releaseWheelDrag?.isDebounced() || this.releaseWheelZoom?.isDebounced()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log('move');
|
||||
|
||||
const e = this.eventUp = getEvent(_e);
|
||||
const xUp = e.clientX;
|
||||
const yUp = e.clientY;
|
||||
|
@ -257,18 +390,109 @@ export default class SwipeHandler {
|
|||
return;
|
||||
}
|
||||
|
||||
this.hadMove = true;
|
||||
this.setHadMove(_e);
|
||||
}
|
||||
|
||||
if(!IS_TOUCH_SUPPORTED) {
|
||||
this.setCursorTo.style.setProperty('cursor', this.cursor, 'important');
|
||||
const touches = (_e as any as TouchEvent).touches;
|
||||
if(this.onZoom && this.initialDistance > 0 && touches.length === 2) {
|
||||
const endDistance = getDistance(touches[0], touches[1]);
|
||||
const touchCenter = getTouchCenter(touches[0], touches[1]);
|
||||
const dragOffsetX = touchCenter.x - this.initialTouchCenter.x;
|
||||
const dragOffsetY = touchCenter.y - this.initialTouchCenter.y;
|
||||
const zoomFactor = endDistance / this.initialDistance;
|
||||
const details: ZoomDetails = {
|
||||
zoomFactor,
|
||||
initialCenterX: this.initialTouchCenter.x,
|
||||
initialCenterY: this.initialTouchCenter.y,
|
||||
dragOffsetX,
|
||||
dragOffsetY,
|
||||
currentCenterX: touchCenter.x,
|
||||
currentCenterY: touchCenter.y
|
||||
};
|
||||
|
||||
this.onZoom(details);
|
||||
}
|
||||
|
||||
this.dispatchOnSwipe(xDiff, yDiff, _e);
|
||||
};
|
||||
|
||||
protected handleWheel = (e: WheelEvent) => {
|
||||
if(!this.hadMove && this.verifyTouchTarget) {
|
||||
const result = this.verifyTouchTarget(e);
|
||||
if(result !== undefined && !result) {
|
||||
this.reset(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cancelEvent(e);
|
||||
|
||||
this.log('wheel');
|
||||
|
||||
if(this.onDoubleClick && Object.is(e.deltaX, -0) && Object.is(e.deltaY, -0) && e.ctrlKey) {
|
||||
this.onWheelCapture(e);
|
||||
this.onDoubleClick({centerX: e.pageX, centerY: e.pageY});
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const metaKeyPressed = e.metaKey || e.ctrlKey || e.shiftKey;
|
||||
if(metaKeyPressed) {
|
||||
// * fix zooming while dragging is in inertia
|
||||
if(this.releaseWheelDrag?.isDebounced()) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
this.onFirstSwipe?.(_e);
|
||||
}
|
||||
|
||||
const onSwipeResult = this.onSwipe(xDiff, yDiff, _e);
|
||||
if(onSwipeResult !== undefined && onSwipeResult) {
|
||||
this.reset();
|
||||
this.onWheelZoom(e);
|
||||
} else {
|
||||
this.handleWheelDrag(e);
|
||||
}
|
||||
};
|
||||
|
||||
protected handleWheelDrag = (e: WheelEvent) => {
|
||||
this.log('wheel drag');
|
||||
|
||||
this.onWheelCapture(e);
|
||||
// Ignore wheel inertia if drag is canceled in this direction
|
||||
if(!this.isDragCanceled.x || Math.sign(this.initialDragOffset.x) === Math.sign(e.deltaX)) {
|
||||
this.initialDragOffset.x -= e.deltaX;
|
||||
}
|
||||
if(!this.isDragCanceled.y || Math.sign(this.initialDragOffset.y) === Math.sign(e.deltaY)) {
|
||||
this.initialDragOffset.y -= e.deltaY;
|
||||
}
|
||||
const {x, y} = this.initialDragOffset;
|
||||
this.releaseWheelDrag(e);
|
||||
this.dispatchOnSwipe(x, y, e, (dx, dy) => {
|
||||
this.isDragCanceled = {x: dx, y: dy};
|
||||
});
|
||||
};
|
||||
|
||||
protected onWheelCapture = (e: WheelEvent) => {
|
||||
if(this.hadMove) return;
|
||||
this.log('wheel capture');
|
||||
this.handleStart(e);
|
||||
this.setHadMove(e);
|
||||
this.initialTouchCenter = {x: e.x, y: e.y};
|
||||
};
|
||||
|
||||
protected onWheelZoom = (e: WheelEvent) => {
|
||||
if(!this.onZoom) return;
|
||||
this.log('wheel zoom');
|
||||
this.onWheelCapture(e);
|
||||
const dragOffsetX = e.x - this.initialTouchCenter.x;
|
||||
const dragOffsetY = e.y - this.initialTouchCenter.y;
|
||||
const delta = clamp(e.deltaY, -25, 25);
|
||||
this.wheelZoom -= delta * 0.01;
|
||||
const details: ZoomDetails = {
|
||||
zoomAdd: this.wheelZoom - 1,
|
||||
initialCenterX: this.initialTouchCenter.x,
|
||||
initialCenterY: this.initialTouchCenter.y,
|
||||
dragOffsetX,
|
||||
dragOffsetY,
|
||||
currentCenterX: e.x,
|
||||
currentCenterY: e.y
|
||||
};
|
||||
this.onZoom(details);
|
||||
this.releaseWheelZoom(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* Lethargy help distinguish between scroll events initiated by the user, and those by inertial scrolling.
|
||||
* Lethargy does not have external dependencies.
|
||||
*
|
||||
* @param stability - Specifies the length of the rolling average.
|
||||
* In effect, the larger the value, the smoother the curve will be.
|
||||
* This attempts to prevent anomalies from firing 'real' events. Valid values are all positive integers,
|
||||
* but in most cases, you would need to stay between 5 and around 30.
|
||||
*
|
||||
* @param sensitivity - Specifies the minimum value for wheelDelta for it to register as a valid scroll event.
|
||||
* Because the tail of the curve have low wheelDelta values,
|
||||
* this will stop them from registering as valid scroll events.
|
||||
* The unofficial standard wheelDelta is 120, so valid values are positive integers below 120.
|
||||
*
|
||||
* @param tolerance - Prevent small fluctuations from affecting results.
|
||||
* Valid values are decimals from 0, but should ideally be between 0.05 and 0.3.
|
||||
*
|
||||
* Based on https://github.com/d4nyll/lethargy
|
||||
*/
|
||||
|
||||
export type LethargyConfig = {
|
||||
stability?: number;
|
||||
sensitivity?: number;
|
||||
tolerance?: number;
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
export class Lethargy {
|
||||
private stability: number;
|
||||
|
||||
private sensitivity: number;
|
||||
|
||||
private tolerance: number;
|
||||
|
||||
private delay: number;
|
||||
|
||||
private lastUpDeltas: Array<number>;
|
||||
|
||||
private lastDownDeltas: Array<number>;
|
||||
|
||||
private deltasTimestamp: Array<number>;
|
||||
|
||||
constructor({
|
||||
stability = 8,
|
||||
sensitivity = 100,
|
||||
tolerance = 1.1,
|
||||
delay = 150
|
||||
}: LethargyConfig = {}) {
|
||||
this.stability = stability;
|
||||
this.sensitivity = sensitivity;
|
||||
this.tolerance = tolerance;
|
||||
this.delay = delay;
|
||||
this.lastUpDeltas = new Array(this.stability * 2).fill(0);
|
||||
this.lastDownDeltas = new Array(this.stability * 2).fill(0);
|
||||
this.deltasTimestamp = new Array(this.stability * 2).fill(0);
|
||||
}
|
||||
|
||||
check(e: any) {
|
||||
let lastDelta;
|
||||
e = e.originalEvent || e;
|
||||
if(e.wheelDelta !== undefined) {
|
||||
lastDelta = e.wheelDelta;
|
||||
} else if(e.deltaY !== undefined) {
|
||||
lastDelta = e.deltaY * -40;
|
||||
} else if(e.detail !== undefined || e.detail === 0) {
|
||||
lastDelta = e.detail * -40;
|
||||
}
|
||||
this.deltasTimestamp.push(Date.now());
|
||||
this.deltasTimestamp.shift();
|
||||
if(lastDelta > 0) {
|
||||
this.lastUpDeltas.push(lastDelta);
|
||||
this.lastUpDeltas.shift();
|
||||
return this.isInertia(1);
|
||||
} else {
|
||||
this.lastDownDeltas.push(lastDelta);
|
||||
this.lastDownDeltas.shift();
|
||||
return this.isInertia(-1);
|
||||
}
|
||||
}
|
||||
|
||||
isInertia(direction: number) {
|
||||
const lastDeltas = direction === -1 ? this.lastDownDeltas : this.lastUpDeltas;
|
||||
if(lastDeltas[0] === undefined) return direction;
|
||||
if(
|
||||
this.deltasTimestamp[this.stability * 2 - 2] + this.delay > Date.now() &&
|
||||
lastDeltas[0] === lastDeltas[this.stability * 2 - 1]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const lastDeltasOld = lastDeltas.slice(0, this.stability);
|
||||
const lastDeltasNew = lastDeltas.slice(this.stability, this.stability * 2);
|
||||
const oldSum = lastDeltasOld.reduce((t, s) => t + s);
|
||||
const newSum = lastDeltasNew.reduce((t, s) => t + s);
|
||||
const oldAverage = oldSum / lastDeltasOld.length;
|
||||
const newAverage = newSum / lastDeltasNew.length;
|
||||
return Math.abs(oldAverage) < Math.abs(newAverage * this.tolerance) &&
|
||||
this.sensitivity < Math.abs(newAverage);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
export default function clamp(v: number, min: number, max: number): number {
|
||||
return v < min ? min : ((v > max) ? max : v);
|
||||
return Math.min(max, Math.max(min, v));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export default function isBetween(num: number, min: number, max: number) {
|
||||
return num >= min && num <= max;
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import {MOUNT_CLASS_TO} from '../config/debug';
|
||||
import {IS_WORKER} from './context';
|
||||
|
||||
export class WindowSize {
|
||||
|
@ -15,11 +16,10 @@ export class WindowSize {
|
|||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const w: any = 'visualViewport' in window ? window.visualViewport : window;
|
||||
const w = 'visualViewport' in window ? window.visualViewport : window;
|
||||
const set = () => {
|
||||
this.width = w.width || w.innerWidth;
|
||||
this.height = w.height || w.innerHeight;
|
||||
this.width = w.width || (w as any as Window).innerWidth;
|
||||
this.height = w.height || (w as any as Window).innerHeight;
|
||||
};
|
||||
w.addEventListener('resize', set);
|
||||
set();
|
||||
|
@ -27,4 +27,5 @@ export class WindowSize {
|
|||
}
|
||||
|
||||
const windowSize = new WindowSize();
|
||||
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.windowSize = windowSize);
|
||||
export default windowSize;
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
<meta charset="utf-8">
|
||||
<title>{{htmlWebpackPlugin.options.title}}</title>
|
||||
<meta name="description" content="{{htmlWebpackPlugin.options.description}}">
|
||||
<!--<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no,viewport-fit=cover">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-title" content="{{htmlWebpackPlugin.options.title}}">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
|
|
@ -559,6 +559,14 @@ $inactive-opacity: .4;
|
|||
}
|
||||
}
|
||||
|
||||
&.is-zooming {
|
||||
.media-viewer-caption,
|
||||
.media-viewer-switcher .tgico-down {
|
||||
opacity: 0 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.highlight-switchers {
|
||||
.media-viewer-switcher > span {
|
||||
opacity: 1;
|
||||
|
@ -584,12 +592,29 @@ $inactive-opacity: .4;
|
|||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 4;
|
||||
// transform: matrix(1, 0, 0, 1, 0, 0);
|
||||
transform: translate3d(1, 1, 0) scale(1);
|
||||
will-change: transform;
|
||||
transform-origin: 0 0;
|
||||
|
||||
@include animation-level(2) {
|
||||
transition: transform var(--open-duration);
|
||||
}
|
||||
}
|
||||
|
||||
// &-whole {
|
||||
// &:after {
|
||||
// content: " ";
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// left: 50%;
|
||||
// background-color: green;
|
||||
// z-index: 500;
|
||||
// width: 1px;
|
||||
// }
|
||||
// }
|
||||
|
||||
/* &-switchers {
|
||||
position: relative;
|
||||
width: $large-screen;
|
||||
|
|
|
@ -195,7 +195,7 @@ module.exports = {
|
|||
// },
|
||||
compress: true,
|
||||
http2: useLocalNotLocal ? true : (useLocal ? undefined : true),
|
||||
https: useLocal ? undefined : { // generated keys using mkcert
|
||||
https: useLocal && false ? undefined : { // generated keys using mkcert
|
||||
key: fs.readFileSync(__dirname + '/certs/localhost-key.pem', 'utf8'),
|
||||
cert: fs.readFileSync(__dirname + '/certs/localhost.pem', 'utf8')
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue