1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-27 15:54:39 +00:00
Files
seahub/frontend/src/components/file-view/image-zoomer.js
2025-07-23 18:30:09 +08:00

187 lines
6.2 KiB
JavaScript

import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Input, Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
import IconButton from '../../components/icon-button';
import { gettext } from '../../utils/constants';
const SCALE_OPTIONS = [0.15, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4];
const SCALE_MIN = SCALE_OPTIONS[0];
const SCALE_MAX = SCALE_OPTIONS[SCALE_OPTIONS.length - 1];
const SCALE_OPTIONS_2 = [
{ value: 'page-fit', text: gettext('Page fit') },
{ value: 'actual-size', text: gettext('Actual size') }
];
const DEFAULT_SCALE = SCALE_OPTIONS_2[0];
const ImageZoomer = ({ setImageScale, setDefaultPageFitScale }) => {
const [curScale, setScale] = useState(1);
const [curScaleText, setScaleText] = useState(DEFAULT_SCALE.text); // for the text shown in the input
const [selectedScale, setSelectedScale] = useState(DEFAULT_SCALE.value); // for the scale menu
const [isScaleMenuOpen, setScaleMenuOpen] = useState(false);
const toggleMenu = useCallback(() => {
setScaleMenuOpen(!isScaleMenuOpen);
}, [isScaleMenuOpen, setScaleMenuOpen]);
const scaleImage = useCallback((scale) => {
setImageScale(scale);
}, [setImageScale]);
const zoomInOrOut = useCallback((scale) => {
setScale(scale);
scaleImage(scale);
setScaleText(`${Math.round(scale * 100)}%`); // handle the result of floating point arithmetic
if (SCALE_OPTIONS.indexOf(scale) == -1) {
setSelectedScale(null);
} else {
setSelectedScale(scale);
}
}, [scaleImage]);
const zoomIn = useCallback(() => {
const offset = Math.ceil(curScale) * 0.1; // round up
const normalizedScale = Number((curScale + offset).toFixed(2)); // handle the result of floating point arithmetic
const scale = Math.min(normalizedScale, SCALE_MAX);
zoomInOrOut(scale);
}, [curScale, zoomInOrOut]);
const zoomOut = useCallback(() => {
const offset = Math.ceil(curScale) * 0.1; // round up
const normalizedScale = Number((curScale - offset).toFixed(2)); // handle the result of floating point arithmetic
const scale = Math.max(normalizedScale, SCALE_MIN);
zoomInOrOut(scale);
}, [curScale, zoomInOrOut]);
const scaleImageToPageFit = useCallback(() => {
const imageElement = document.getElementById('image-view');
// make sure real dom is rendered before calculating the scale
if (!imageElement || (imageElement.clientWidth === 2 && imageElement.clientHeight === 2)) {
setTimeout(() => {
scaleImageToPageFit();
}, 1);
return;
}
const borderWidth = 1;
const width = imageElement.clientWidth + borderWidth * 2;
const height = imageElement.clientHeight + borderWidth * 2;
const imageContainer = imageElement.parentNode;
const hPadding = 0; // horizontal padding
const vPadding = 30; // vertical padding
const maxWidth = imageContainer.clientWidth - hPadding * 2;
const maxHeight = imageContainer.clientHeight - vPadding * 2;
const hScale = maxWidth / width;
const vScale = maxHeight / height;
const scale = Math.min(hScale, vScale);
setScale(scale);
scaleImage(scale);
setDefaultPageFitScale(scale);
}, [setScale, scaleImage, setDefaultPageFitScale]);
const onMenuItemClick = useCallback((value) => {
setSelectedScale(value);
if (SCALE_OPTIONS.indexOf(value) != -1) {
const scale = value;
setScale(scale);
scaleImage(scale);
setScaleText(`${scale * 100}%`);
} else {
if (value == 'actual-size') {
const scale = 1;
setScale(scale);
scaleImage(scale);
} else {
// 'page-fit'
scaleImageToPageFit();
}
setScaleText(SCALE_OPTIONS_2.filter(item => item.value == value)[0].text);
}
setScaleMenuOpen(false);
}, [scaleImage, setScaleMenuOpen, scaleImageToPageFit]);
const onMenuItemKeyDown = useCallback((e, value) => {
if (e.key == 'Enter' || e.key == 'Space') {
onMenuItemClick(value);
}
}, [onMenuItemClick]);
useEffect(() => {
scaleImageToPageFit();
}, [scaleImageToPageFit]);
return (
<div className='d-flex align-items-center image-zoomer'>
<IconButton
id="zoom-out-image"
icon="minus_sign"
text={gettext('Zoom out')}
onClick={zoomOut}
disabled={curScale == SCALE_MIN}
/>
<Dropdown
isOpen={isScaleMenuOpen}
toggle={toggleMenu}
className="vam"
direction='down'
>
<DropdownToggle
tag='div'
className="position-relative"
data-toggle="dropdown"
aria-expanded={isScaleMenuOpen}
>
<Input id="cur-scale-input" type="text" value={curScaleText} readOnly={true} />
<i id="scale-menu-caret" className="sf3-font-down sf3-font"></i>
</DropdownToggle>
<DropdownMenu id="scale-menu">
{SCALE_OPTIONS.map((item, index) => {
return (
<DropdownItem
key={index}
className="position-relative pl-5"
onClick={() => {onMenuItemClick(item);}}
onKeyDown={(e) => {onMenuItemKeyDown(e, item);}}
>
{selectedScale == item && (
<i className="dropdown-item-tick sf2-icon-tick"></i>
)}
<span>{`${item * 100}%`}</span>
</DropdownItem>
);
})}
{SCALE_OPTIONS_2.map((item, index) => {
return (
<DropdownItem
key={index}
className="position-relative pl-5"
onClick={() => {onMenuItemClick(item.value);}}
onKeyDown={(e) => {onMenuItemKeyDown(e, item.value);}}
>
{selectedScale == item.value && (
<i className="dropdown-item-tick sf2-icon-tick"></i>
)}
<span>{item.text}</span>
</DropdownItem>
);
})}
</DropdownMenu>
</Dropdown>
<IconButton
id="zoom-in-image"
icon="plus_sign"
text={gettext('Zoom in')}
onClick={zoomIn}
disabled={curScale == SCALE_MAX}
/>
</div>
);
};
ImageZoomer.propTypes = {
setImageScale: PropTypes.func
};
export default ImageZoomer;