mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-26 15:26:19 +00:00
[image file view] add a new version of 'image zoomer'(zoom in/out icons, and a scale menu) (#8059)
This commit is contained in:
@@ -82,7 +82,7 @@ class FileContent extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="file-view-content flex-1 image-file-view">
|
||||
<div className="file-view-content flex-1 image-file-view d-flex align-items-center justify-content-center">
|
||||
{previousImage && (
|
||||
<a href={previousImageUrl} id="img-prev" title={gettext('you can also press ← ')}><span className="sf3-font sf3-font-down rotate-90 d-inline-block"></span></a>
|
||||
)}
|
||||
|
@@ -1,57 +1,167 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Input } from 'reactstrap';
|
||||
import Icon from '../../components/icon';
|
||||
import { Input, Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||
import IconButton from '../../components/icon-button';
|
||||
import { gettext } from '../../utils/constants';
|
||||
|
||||
import '../../metadata/components/data-process-setter/gallery-slider-setter/index.css';
|
||||
|
||||
const SCALE_OPTIONS = [0.25, 0.5, 1, 1.5, 2];
|
||||
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 ImageZoomer = ({ setImageScale }) => {
|
||||
|
||||
const [curScale, setScale] = useState(1);
|
||||
const [curScaleText, setScaleText] = useState(`${curScale * 100}%`); // for the text shown in the input
|
||||
const [selectedScale, setSelectedScale] = useState(1); // for the scale menu
|
||||
const [isScaleMenuOpen, setScaleMenuOpen] = useState(false);
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
setScaleMenuOpen(!isScaleMenuOpen);
|
||||
}, [isScaleMenuOpen, setScaleMenuOpen]);
|
||||
|
||||
const scaleImage = useCallback((scale) => {
|
||||
setImageScale(scale);
|
||||
}, [setImageScale]);
|
||||
|
||||
const changeScale = useCallback((e) => {
|
||||
const scale = Number(e.target.value);
|
||||
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 scale = SCALE_OPTIONS[SCALE_OPTIONS.indexOf(curScale) + 1];
|
||||
setScale(scale);
|
||||
scaleImage(scale);
|
||||
}, [curScale, scaleImage]);
|
||||
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 scale = SCALE_OPTIONS[SCALE_OPTIONS.indexOf(curScale) - 1];
|
||||
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');
|
||||
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);
|
||||
}, [curScale, scaleImage]);
|
||||
}, [setScale, scaleImage]);
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<div className='metadata-slider-container image-zoomer ml-0'>
|
||||
<Button className="metadata-slider-icon-button" onClick={zoomOut} disabled={curScale == SCALE_MIN}>
|
||||
<Icon symbol='minus_sign' className='metadata-slider-icon' />
|
||||
</Button>
|
||||
<Input
|
||||
type="range"
|
||||
min={SCALE_MIN}
|
||||
max={SCALE_MAX}
|
||||
step="any"
|
||||
value={curScale}
|
||||
onChange={changeScale}
|
||||
className="metadata-slider"
|
||||
<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}
|
||||
/>
|
||||
<Button className="metadata-slider-icon-button" onClick={zoomIn} disabled={curScale == SCALE_MAX}>
|
||||
<Icon symbol='plus_sign' className='metadata-slider-icon' />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -18,8 +18,6 @@
|
||||
border: 1px solid #eee;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: calc(100% - 4px);
|
||||
max-height: calc(100% - 4px);
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
@@ -51,6 +49,32 @@
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.image-zoomer {
|
||||
height: 24px;
|
||||
.image-zoomer #zoom-out-image {
|
||||
margin-right: 2px;
|
||||
font-size: 18px; /* the actual icon is smaller than the others */
|
||||
}
|
||||
|
||||
.image-zoomer #zoom-in-image {
|
||||
margin-left: 2px;
|
||||
font-size: 18px; /* the actual icon is smaller than the others */
|
||||
}
|
||||
|
||||
.image-zoomer #cur-scale-input {
|
||||
height: 28px;
|
||||
width: 120px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.image-zoomer #scale-menu {
|
||||
min-width: 120px; /* overwrite css from seahub_react.css */
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.image-zoomer #scale-menu-caret {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 9px;
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
color: #666;
|
||||
}
|
||||
|
Reference in New Issue
Block a user