mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-01-16 12:43:13 -05:00
7a81082704
Unfortunately the new hammer.js functionality wasn't correctly tested and didn't work across devices and browsers, as such, it's best to revert PR #6944 until we can revisit this functionality and make it work across all devices and browsers that are supported by Mastodon. This reverts commit 5021c4e9ca78881f5379a18185a46e580b8f2c34.
152 lines
3.8 KiB
JavaScript
152 lines
3.8 KiB
JavaScript
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
|
|
const MIN_SCALE = 1;
|
|
const MAX_SCALE = 4;
|
|
|
|
const getMidpoint = (p1, p2) => ({
|
|
x: (p1.clientX + p2.clientX) / 2,
|
|
y: (p1.clientY + p2.clientY) / 2,
|
|
});
|
|
|
|
const getDistance = (p1, p2) =>
|
|
Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2));
|
|
|
|
const clamp = (min, max, value) => Math.min(max, Math.max(min, value));
|
|
|
|
export default class ZoomableImage extends React.PureComponent {
|
|
|
|
static propTypes = {
|
|
alt: PropTypes.string,
|
|
src: PropTypes.string.isRequired,
|
|
width: PropTypes.number,
|
|
height: PropTypes.number,
|
|
onClick: PropTypes.func,
|
|
}
|
|
|
|
static defaultProps = {
|
|
alt: '',
|
|
width: null,
|
|
height: null,
|
|
};
|
|
|
|
state = {
|
|
scale: MIN_SCALE,
|
|
}
|
|
|
|
removers = [];
|
|
container = null;
|
|
image = null;
|
|
lastTouchEndTime = 0;
|
|
lastDistance = 0;
|
|
|
|
componentDidMount () {
|
|
let handler = this.handleTouchStart;
|
|
this.container.addEventListener('touchstart', handler);
|
|
this.removers.push(() => this.container.removeEventListener('touchstart', handler));
|
|
handler = this.handleTouchMove;
|
|
// on Chrome 56+, touch event listeners will default to passive
|
|
// https://www.chromestatus.com/features/5093566007214080
|
|
this.container.addEventListener('touchmove', handler, { passive: false });
|
|
this.removers.push(() => this.container.removeEventListener('touchend', handler));
|
|
}
|
|
|
|
componentWillUnmount () {
|
|
this.removeEventListeners();
|
|
}
|
|
|
|
removeEventListeners () {
|
|
this.removers.forEach(listeners => listeners());
|
|
this.removers = [];
|
|
}
|
|
|
|
handleTouchStart = e => {
|
|
if (e.touches.length !== 2) return;
|
|
|
|
this.lastDistance = getDistance(...e.touches);
|
|
}
|
|
|
|
handleTouchMove = e => {
|
|
const { scrollTop, scrollHeight, clientHeight } = this.container;
|
|
if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) {
|
|
// prevent propagating event to MediaModal
|
|
e.stopPropagation();
|
|
return;
|
|
}
|
|
if (e.touches.length !== 2) return;
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const distance = getDistance(...e.touches);
|
|
const midpoint = getMidpoint(...e.touches);
|
|
const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance);
|
|
|
|
this.zoom(scale, midpoint);
|
|
|
|
this.lastMidpoint = midpoint;
|
|
this.lastDistance = distance;
|
|
}
|
|
|
|
zoom(nextScale, midpoint) {
|
|
const { scale } = this.state;
|
|
const { scrollLeft, scrollTop } = this.container;
|
|
|
|
// math memo:
|
|
// x = (scrollLeft + midpoint.x) / scrollWidth
|
|
// x' = (nextScrollLeft + midpoint.x) / nextScrollWidth
|
|
// scrollWidth = clientWidth * scale
|
|
// scrollWidth' = clientWidth * nextScale
|
|
// Solve x = x' for nextScrollLeft
|
|
const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x;
|
|
const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y;
|
|
|
|
this.setState({ scale: nextScale }, () => {
|
|
this.container.scrollLeft = nextScrollLeft;
|
|
this.container.scrollTop = nextScrollTop;
|
|
});
|
|
}
|
|
|
|
handleClick = e => {
|
|
// don't propagate event to MediaModal
|
|
e.stopPropagation();
|
|
const handler = this.props.onClick;
|
|
if (handler) handler();
|
|
}
|
|
|
|
setContainerRef = c => {
|
|
this.container = c;
|
|
}
|
|
|
|
setImageRef = c => {
|
|
this.image = c;
|
|
}
|
|
|
|
render () {
|
|
const { alt, src } = this.props;
|
|
const { scale } = this.state;
|
|
const overflow = scale === 1 ? 'hidden' : 'scroll';
|
|
|
|
return (
|
|
<div
|
|
className='zoomable-image'
|
|
ref={this.setContainerRef}
|
|
style={{ overflow }}
|
|
>
|
|
<img
|
|
role='presentation'
|
|
ref={this.setImageRef}
|
|
alt={alt}
|
|
src={src}
|
|
style={{
|
|
transform: `scale(${scale})`,
|
|
transformOrigin: '0 0',
|
|
}}
|
|
onClick={this.handleClick}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
}
|