mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
Merge c671dc8846 into 7e75035cb6
This commit is contained in:
commit
76340d8195
13 changed files with 521 additions and 31 deletions
|
|
@ -132,12 +132,15 @@ exports[`components/FilePreviewModal should match snapshot, loaded 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="file-preview-modal__content"
|
||||
class="file-preview-modal__content file-preview-modal__content-with-image-controls"
|
||||
>
|
||||
<div>
|
||||
Image Preview
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Image Controls Bar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -161,12 +164,15 @@ exports[`components/FilePreviewModal should match snapshot, loaded and showing f
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="file-preview-modal__content"
|
||||
class="file-preview-modal__content file-preview-modal__content-with-image-controls"
|
||||
>
|
||||
<div>
|
||||
Image Preview
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Image Controls Bar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -277,12 +283,15 @@ exports[`components/FilePreviewModal should match snapshot, loaded with footer 1
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="file-preview-modal__content"
|
||||
class="file-preview-modal__content file-preview-modal__content-with-image-controls"
|
||||
>
|
||||
<div>
|
||||
Image Preview
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Image Controls Bar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -306,12 +315,15 @@ exports[`components/FilePreviewModal should match snapshot, loaded with image 1`
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="file-preview-modal__content"
|
||||
class="file-preview-modal__content file-preview-modal__content-with-image-controls"
|
||||
>
|
||||
<div>
|
||||
Image Preview
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Image Controls Bar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,24 +2,26 @@
|
|||
|
||||
exports[`components/view_image/ImagePreview should match snapshot, with preview 1`] = `
|
||||
<div>
|
||||
<a
|
||||
<div
|
||||
class="image_preview"
|
||||
href="#"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
alt="preview url image"
|
||||
class="image_preview__image"
|
||||
data-testid="imagePreview"
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
src="/api/v4/files/file_id_1/preview"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/view_image/ImagePreview should match snapshot, with preview, cannot download 1`] = `
|
||||
<div>
|
||||
<img
|
||||
draggable="false"
|
||||
src="/api/v4/files/file_id_1/preview"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -27,24 +29,26 @@ exports[`components/view_image/ImagePreview should match snapshot, with preview,
|
|||
|
||||
exports[`components/view_image/ImagePreview should match snapshot, without preview 1`] = `
|
||||
<div>
|
||||
<a
|
||||
<div
|
||||
class="image_preview"
|
||||
href="#"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
alt="preview url image"
|
||||
class="image_preview__image"
|
||||
data-testid="imagePreview"
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
src="/api/v4/files/file_id?download=1"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/view_image/ImagePreview should match snapshot, without preview, cannot download 1`] = `
|
||||
<div>
|
||||
<img
|
||||
draggable="false"
|
||||
src="/api/v4/files/file_id?download=1"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__content-with-image-controls {
|
||||
height: calc(100vh - 112px);
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
height: calc(100% - 148px);
|
||||
}
|
||||
}
|
||||
|
||||
&__content-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&__content-scrollable {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
@ -62,6 +76,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__image-controls-bar {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.modal.fade & {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ jest.mock('components/file_preview_modal/file_preview_modal_header/file_preview_
|
|||
<div>{'File Preview Modal Header'}</div>
|
||||
));
|
||||
jest.mock('components/file_preview_modal/image_preview', () => () => <div>{'Image Preview'}</div>);
|
||||
jest.mock('components/file_preview_modal/image_controls_bar', () => () => <div>{'Image Controls Bar'}</div>);
|
||||
jest.mock('components/file_preview_modal/popover_bar', () => () => <div>{'Popover Bar'}</div>);
|
||||
|
||||
describe('components/FilePreviewModal', () => {
|
||||
|
|
@ -146,9 +147,9 @@ describe('components/FilePreviewModal', () => {
|
|||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should go to next or previous upon key press of right or left, respectively', () => {
|
||||
test('should go to next or previous upon key press of right or left, respectively for non-image preview', () => {
|
||||
const fileInfos = [
|
||||
TestHelper.getFileInfoMock({id: 'file_id_1', extension: 'gif'}),
|
||||
TestHelper.getFileInfoMock({id: 'file_id_1', extension: 'txt'}),
|
||||
TestHelper.getFileInfoMock({id: 'file_id_2', extension: 'wma'}),
|
||||
TestHelper.getFileInfoMock({id: 'file_id_3', extension: 'mp4'}),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import type {FilePreviewComponent} from 'types/store/plugins';
|
|||
import FilePreviewModalFooter from './file_preview_modal_footer/file_preview_modal_footer';
|
||||
import FilePreviewModalHeader from './file_preview_modal_header/file_preview_modal_header';
|
||||
import ImagePreview from './image_preview';
|
||||
import ImageControlsBar from './image_controls_bar';
|
||||
import PopoverBar from './popover_bar';
|
||||
import {isFileInfo, isLinkInfo} from './types';
|
||||
import type {LinkInfo} from './types';
|
||||
|
|
@ -72,6 +73,12 @@ type State = {
|
|||
showCloseBtn: boolean;
|
||||
showZoomControls: boolean;
|
||||
scale: Record<number, number>;
|
||||
rotation: Record<number, number>;
|
||||
flipHorizontal: Record<number, boolean>;
|
||||
flipVertical: Record<number, boolean>;
|
||||
translateX: Record<number, number>;
|
||||
translateY: Record<number, number>;
|
||||
isDraggingImage: boolean;
|
||||
content: string;
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +89,11 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
|
|||
pluginFilePreviewComponents: [],
|
||||
};
|
||||
|
||||
private dragStartX = 0;
|
||||
private dragStartY = 0;
|
||||
private dragBaseX = 0;
|
||||
private dragBaseY = 0;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
|
|
@ -95,6 +107,12 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
|
|||
showCloseBtn: false,
|
||||
showZoomControls: false,
|
||||
scale: Utils.fillRecord(ZoomSettings.DEFAULT_SCALE, this.props.fileInfos.length),
|
||||
rotation: Utils.fillRecord(0, this.props.fileInfos.length),
|
||||
flipHorizontal: Utils.fillRecord(false, this.props.fileInfos.length),
|
||||
flipVertical: Utils.fillRecord(false, this.props.fileInfos.length),
|
||||
translateX: Utils.fillRecord(0, this.props.fileInfos.length),
|
||||
translateY: Utils.fillRecord(0, this.props.fileInfos.length),
|
||||
isDraggingImage: false,
|
||||
content: '',
|
||||
};
|
||||
}
|
||||
|
|
@ -115,7 +133,11 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
|
|||
this.showImage(id);
|
||||
};
|
||||
|
||||
handleKeyPress = (e: KeyboardEvent) => {
|
||||
handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (this.isCurrentImagePreview()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.RIGHT)) {
|
||||
this.handleNext();
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.LEFT)) {
|
||||
|
|
@ -123,14 +145,26 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
|
|||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (e: KeyboardEvent) => {
|
||||
this.handleImagePanByKeyboard(e);
|
||||
};
|
||||
|
||||
handleKeyPress = (e: KeyboardEvent) => {
|
||||
this.handleKeyUp(e);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keyup', this.handleKeyPress);
|
||||
document.addEventListener('keyup', this.handleKeyUp);
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
this.showImage(this.props.startIndex);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keyup', this.handleKeyPress);
|
||||
document.removeEventListener('keyup', this.handleKeyUp);
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
window.removeEventListener('mousemove', this.handleImageMouseMove);
|
||||
window.removeEventListener('mouseup', this.handleImageMouseUp);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Props, state: State) {
|
||||
|
|
@ -143,6 +177,13 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
|
|||
if (props.fileInfos.length !== state.prevFileInfosCount) {
|
||||
updatedState.loaded = Utils.fillRecord(false, props.fileInfos.length);
|
||||
updatedState.progress = Utils.fillRecord(0, props.fileInfos.length);
|
||||
updatedState.scale = Utils.fillRecord(ZoomSettings.DEFAULT_SCALE, props.fileInfos.length);
|
||||
updatedState.rotation = Utils.fillRecord(0, props.fileInfos.length);
|
||||
updatedState.flipHorizontal = Utils.fillRecord(false, props.fileInfos.length);
|
||||
updatedState.flipVertical = Utils.fillRecord(false, props.fileInfos.length);
|
||||
updatedState.translateX = Utils.fillRecord(0, props.fileInfos.length);
|
||||
updatedState.translateY = Utils.fillRecord(0, props.fileInfos.length);
|
||||
updatedState.isDraggingImage = false;
|
||||
updatedState.prevFileInfosCount = props.fileInfos.length;
|
||||
}
|
||||
return Object.keys(updatedState).length ? updatedState : null;
|
||||
|
|
@ -260,13 +301,13 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
|
|||
};
|
||||
|
||||
handleZoomIn = () => {
|
||||
let newScale = this.state.scale[this.state.imageIndex];
|
||||
let newScale = this.state.scale[this.state.imageIndex] ?? ZoomSettings.DEFAULT_SCALE;
|
||||
newScale = Math.min(newScale + ZoomSettings.SCALE_DELTA, ZoomSettings.MAX_SCALE);
|
||||
this.setScale(this.state.imageIndex, newScale);
|
||||
};
|
||||
|
||||
handleZoomOut = () => {
|
||||
let newScale = this.state.scale[this.state.imageIndex];
|
||||
let newScale = this.state.scale[this.state.imageIndex] ?? ZoomSettings.DEFAULT_SCALE;
|
||||
newScale = Math.max(newScale - ZoomSettings.SCALE_DELTA, ZoomSettings.MIN_SCALE);
|
||||
this.setScale(this.state.imageIndex, newScale);
|
||||
};
|
||||
|
|
@ -275,6 +316,162 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
|
|||
this.setScale(this.state.imageIndex, ZoomSettings.DEFAULT_SCALE);
|
||||
};
|
||||
|
||||
handleImageWheel = (e: React.WheelEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.deltaY < 0) {
|
||||
this.handleZoomIn();
|
||||
} else if (e.deltaY > 0) {
|
||||
this.handleZoomOut();
|
||||
}
|
||||
};
|
||||
|
||||
private setRotation = (index: number, rotation: number) => {
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
rotation: {
|
||||
...prevState.rotation,
|
||||
[index]: rotation,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
handleRotateClockwise = () => {
|
||||
const currentRotation = this.state.rotation[this.state.imageIndex] ?? 0;
|
||||
this.setRotation(this.state.imageIndex, (currentRotation + 90) % 360);
|
||||
};
|
||||
|
||||
handleRotateCounterClockwise = () => {
|
||||
const currentRotation = this.state.rotation[this.state.imageIndex] ?? 0;
|
||||
this.setRotation(this.state.imageIndex, (currentRotation + 270) % 360);
|
||||
};
|
||||
|
||||
private toggleFlipHorizontal = () => {
|
||||
this.setState((prevState) => {
|
||||
const index = prevState.imageIndex;
|
||||
return {
|
||||
flipHorizontal: {
|
||||
...prevState.flipHorizontal,
|
||||
[index]: !prevState.flipHorizontal[index],
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
private toggleFlipVertical = () => {
|
||||
this.setState((prevState) => {
|
||||
const index = prevState.imageIndex;
|
||||
return {
|
||||
flipVertical: {
|
||||
...prevState.flipVertical,
|
||||
[index]: !prevState.flipVertical[index],
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
private getImageTransform = (index: number): string => {
|
||||
const zoom = this.state.scale[index] ?? ZoomSettings.DEFAULT_SCALE;
|
||||
const rotation = this.state.rotation[index] ?? 0;
|
||||
const flipX = this.state.flipHorizontal[index] ? -1 : 1;
|
||||
const flipY = this.state.flipVertical[index] ? -1 : 1;
|
||||
const translateX = this.state.translateX[index] ?? 0;
|
||||
const translateY = this.state.translateY[index] ?? 0;
|
||||
|
||||
return `translate(${translateX}px, ${translateY}px) rotate(${rotation}deg) scale(${zoom * flipX}, ${zoom * flipY})`;
|
||||
};
|
||||
|
||||
private setTranslate = (index: number, x: number, y: number) => {
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
translateX: {
|
||||
...prevState.translateX,
|
||||
[index]: x,
|
||||
},
|
||||
translateY: {
|
||||
...prevState.translateY,
|
||||
[index]: y,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
private isCurrentImagePreview = () => {
|
||||
const fileInfo = this.props.fileInfos[this.state.imageIndex];
|
||||
const fileType = this.getFileTypeFromFileInfo(fileInfo);
|
||||
return this.state.loaded[this.state.imageIndex] && (fileType === FileTypes.IMAGE || fileType === FileTypes.SVG);
|
||||
};
|
||||
|
||||
private handleImagePanByKeyboard = (e: KeyboardEvent): boolean => {
|
||||
if (!this.isCurrentImagePreview()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const step = e.shiftKey ? 48 : 24;
|
||||
const currentX = this.state.translateX[this.state.imageIndex] ?? 0;
|
||||
const currentY = this.state.translateY[this.state.imageIndex] ?? 0;
|
||||
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.UP)) {
|
||||
e.preventDefault();
|
||||
this.setTranslate(this.state.imageIndex, currentX, currentY - step);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
e.preventDefault();
|
||||
this.setTranslate(this.state.imageIndex, currentX, currentY + step);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.LEFT)) {
|
||||
e.preventDefault();
|
||||
this.setTranslate(this.state.imageIndex, currentX - step, currentY);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.RIGHT)) {
|
||||
e.preventDefault();
|
||||
this.setTranslate(this.state.imageIndex, currentX + step, currentY);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
private handleImageMouseDown = (e: React.MouseEvent) => {
|
||||
if (e.button !== 0 || !this.isCurrentImagePreview()) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this.dragStartX = e.clientX;
|
||||
this.dragStartY = e.clientY;
|
||||
this.dragBaseX = this.state.translateX[this.state.imageIndex] ?? 0;
|
||||
this.dragBaseY = this.state.translateY[this.state.imageIndex] ?? 0;
|
||||
|
||||
this.setState({isDraggingImage: true});
|
||||
window.addEventListener('mousemove', this.handleImageMouseMove);
|
||||
window.addEventListener('mouseup', this.handleImageMouseUp);
|
||||
};
|
||||
|
||||
private handleImageMouseMove = (e: MouseEvent) => {
|
||||
const deltaX = e.clientX - this.dragStartX;
|
||||
const deltaY = e.clientY - this.dragStartY;
|
||||
this.setTranslate(this.state.imageIndex, this.dragBaseX + deltaX, this.dragBaseY + deltaY);
|
||||
};
|
||||
|
||||
private handleImageMouseUp = () => {
|
||||
this.setState({isDraggingImage: false});
|
||||
window.removeEventListener('mousemove', this.handleImageMouseMove);
|
||||
window.removeEventListener('mouseup', this.handleImageMouseUp);
|
||||
};
|
||||
|
||||
private handlePreventNativeDrag = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
handleModalClose = () => {
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
|
@ -323,6 +520,7 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
|
|||
|
||||
let content;
|
||||
let zoomBar;
|
||||
let imageControlsBar;
|
||||
|
||||
if (isFileInfo(fileInfo) && fileInfo.archived) {
|
||||
content = (
|
||||
|
|
@ -339,6 +537,22 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
|
|||
<ImagePreview
|
||||
fileInfo={fileInfo as FileInfo}
|
||||
canDownloadFiles={this.props.canDownloadFiles}
|
||||
transform={this.getImageTransform(this.state.imageIndex)}
|
||||
/>
|
||||
);
|
||||
const currentScale = this.state.scale[this.state.imageIndex] ?? ZoomSettings.DEFAULT_SCALE;
|
||||
imageControlsBar = (
|
||||
<ImageControlsBar
|
||||
canZoomIn={currentScale < ZoomSettings.MAX_SCALE}
|
||||
canZoomOut={currentScale > ZoomSettings.MIN_SCALE}
|
||||
isFlipHorizontal={Boolean(this.state.flipHorizontal[this.state.imageIndex])}
|
||||
isFlipVertical={Boolean(this.state.flipVertical[this.state.imageIndex])}
|
||||
handleZoomIn={this.handleZoomIn}
|
||||
handleZoomOut={this.handleZoomOut}
|
||||
handleRotateClockwise={this.handleRotateClockwise}
|
||||
handleRotateCounterClockwise={this.handleRotateCounterClockwise}
|
||||
handleFlipHorizontal={this.toggleFlipHorizontal}
|
||||
handleFlipVertical={this.toggleFlipVertical}
|
||||
/>
|
||||
);
|
||||
} else if (fileType === FileTypes.VIDEO || fileType === FileTypes.AUDIO) {
|
||||
|
|
@ -478,12 +692,18 @@ export default class FilePreviewModal extends React.PureComponent<Props, State>
|
|||
'file-preview-modal__content',
|
||||
{
|
||||
'file-preview-modal__content-scrollable': (!isFileInfo(fileInfo) || !fileInfo.archived) && this.state.loaded[this.state.imageIndex] && (fileType === FileTypes.PDF),
|
||||
'file-preview-modal__content-with-image-controls': Boolean(imageControlsBar),
|
||||
'file-preview-modal__content-dragging': this.state.isDraggingImage,
|
||||
},
|
||||
)}
|
||||
onClick={this.handleBgClose}
|
||||
onWheel={imageControlsBar ? this.handleImageWheel : undefined}
|
||||
onMouseDown={imageControlsBar ? this.handleImageMouseDown : undefined}
|
||||
onDragStart={imageControlsBar ? this.handlePreventNativeDrag : undefined}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
{imageControlsBar}
|
||||
{ this.props.isMobileView &&
|
||||
<FilePreviewModalFooter
|
||||
post={this.props.post}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
.file-preview-modal {
|
||||
&__image-controls-bar {
|
||||
.modal .modal-image & {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
.file-preview-modal-image-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.file-preview-modal-main-actions__action-item {
|
||||
color: white;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&__rotate-ccw {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
&__flip-button {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__flip-label {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import './image_controls_bar.scss';
|
||||
|
||||
type Props = {
|
||||
canZoomIn: boolean;
|
||||
canZoomOut: boolean;
|
||||
isFlipHorizontal: boolean;
|
||||
isFlipVertical: boolean;
|
||||
handleZoomIn: () => void;
|
||||
handleZoomOut: () => void;
|
||||
handleRotateClockwise: () => void;
|
||||
handleRotateCounterClockwise: () => void;
|
||||
handleFlipHorizontal: () => void;
|
||||
handleFlipVertical: () => void;
|
||||
}
|
||||
|
||||
export default function ImageControlsBar({
|
||||
canZoomIn,
|
||||
canZoomOut,
|
||||
isFlipHorizontal,
|
||||
isFlipVertical,
|
||||
handleZoomIn,
|
||||
handleZoomOut,
|
||||
handleRotateClockwise,
|
||||
handleRotateCounterClockwise,
|
||||
handleFlipHorizontal,
|
||||
handleFlipVertical,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className='modal-button-bar file-preview-modal__image-controls-bar'>
|
||||
<div className='file-preview-modal-image-controls'>
|
||||
<WithTooltip
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='view_image.zoom_out'
|
||||
defaultMessage='Zoom Out'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='file-preview-modal-main-actions__action-item'
|
||||
onClick={handleZoomOut}
|
||||
aria-label='Zoom out'
|
||||
disabled={!canZoomOut}
|
||||
>
|
||||
<i className='icon icon-minus'/>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
<WithTooltip
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='view_image.zoom_in'
|
||||
defaultMessage='Zoom In'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='file-preview-modal-main-actions__action-item'
|
||||
onClick={handleZoomIn}
|
||||
aria-label='Zoom in'
|
||||
disabled={!canZoomIn}
|
||||
>
|
||||
<i className='icon icon-plus'/>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
<WithTooltip
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='view_image.rotate_counter_clockwise'
|
||||
defaultMessage='Rotate Counter Clockwise'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='file-preview-modal-main-actions__action-item'
|
||||
onClick={handleRotateCounterClockwise}
|
||||
aria-label='Rotate counter clockwise'
|
||||
>
|
||||
<i className='icon icon-refresh file-preview-modal-image-controls__rotate-ccw'/>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
<WithTooltip
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='view_image.rotate_clockwise'
|
||||
defaultMessage='Rotate Clockwise'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='file-preview-modal-main-actions__action-item'
|
||||
onClick={handleRotateClockwise}
|
||||
aria-label='Rotate clockwise'
|
||||
>
|
||||
<i className='icon icon-refresh'/>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
<WithTooltip
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='view_image.flip_horizontal'
|
||||
defaultMessage='Flip Horizontal'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className={classNames(
|
||||
'file-preview-modal-main-actions__action-item',
|
||||
'file-preview-modal-image-controls__flip-button',
|
||||
{'active': isFlipHorizontal},
|
||||
)}
|
||||
onClick={handleFlipHorizontal}
|
||||
aria-label='Flip horizontal'
|
||||
>
|
||||
<span className='file-preview-modal-image-controls__flip-label'>{'H'}</span>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
<WithTooltip
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='view_image.flip_vertical'
|
||||
defaultMessage='Flip Vertical'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className={classNames(
|
||||
'file-preview-modal-main-actions__action-item',
|
||||
'file-preview-modal-image-controls__flip-button',
|
||||
{'active': isFlipVertical},
|
||||
)}
|
||||
onClick={handleFlipVertical}
|
||||
aria-label='Flip vertical'
|
||||
>
|
||||
<span className='file-preview-modal-image-controls__flip-label'>{'V'}</span>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import ImageControlsBar from './image_controls_bar';
|
||||
|
||||
export default ImageControlsBar;
|
||||
|
|
@ -1,8 +1,18 @@
|
|||
.image_preview {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__image {
|
||||
max-height: calc(100vh - 168px);
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 8px;
|
||||
cursor: default;
|
||||
cursor: inherit;
|
||||
transform-origin: center center;
|
||||
transition: transform 150ms ease;
|
||||
user-select: none;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
border-radius: 0;
|
||||
|
|
|
|||
|
|
@ -86,7 +86,20 @@ describe('components/view_image/ImagePreview', () => {
|
|||
<ImagePreview {...props}/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', '#');
|
||||
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('imagePreview')).toHaveAttribute('src', props.fileInfo.link);
|
||||
});
|
||||
|
||||
test('should apply transform style', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
transform: 'rotate(90deg) scale(1.2, 1.2)',
|
||||
};
|
||||
|
||||
render(
|
||||
<ImagePreview {...props}/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('imagePreview')).toHaveStyle({transform: props.transform});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ import './image_preview.scss';
|
|||
interface Props {
|
||||
fileInfo: FileInfo;
|
||||
canDownloadFiles: boolean;
|
||||
transform?: string;
|
||||
}
|
||||
|
||||
export default function ImagePreview({fileInfo, canDownloadFiles}: Props) {
|
||||
export default function ImagePreview({fileInfo, canDownloadFiles, transform}: Props) {
|
||||
const isExternalFile = !fileInfo.id;
|
||||
|
||||
let fileUrl;
|
||||
|
|
@ -30,10 +31,6 @@ export default function ImagePreview({fileInfo, canDownloadFiles}: Props) {
|
|||
previewUrl = fileInfo.has_preview_image ? getFilePreviewUrl(fileInfo.id) : fileUrl;
|
||||
}
|
||||
|
||||
if (!canDownloadFiles) {
|
||||
return <img src={previewUrl}/>;
|
||||
}
|
||||
|
||||
let conditionalSVGStyleAttribute;
|
||||
if (getFileType(fileInfo.extension) === FileTypes.SVG) {
|
||||
conditionalSVGStyleAttribute = {
|
||||
|
|
@ -42,10 +39,26 @@ export default function ImagePreview({fileInfo, canDownloadFiles}: Props) {
|
|||
};
|
||||
}
|
||||
|
||||
const imageStyle = {
|
||||
...conditionalSVGStyleAttribute,
|
||||
transform,
|
||||
};
|
||||
|
||||
if (!canDownloadFiles) {
|
||||
return (
|
||||
<img
|
||||
src={previewUrl}
|
||||
style={imageStyle}
|
||||
draggable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
<div
|
||||
className='image_preview'
|
||||
href='#'
|
||||
draggable={false}
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
>
|
||||
<img
|
||||
className='image_preview__image'
|
||||
|
|
@ -53,8 +66,10 @@ export default function ImagePreview({fileInfo, canDownloadFiles}: Props) {
|
|||
data-testid='imagePreview'
|
||||
alt={'preview url image'}
|
||||
src={previewUrl}
|
||||
style={conditionalSVGStyleAttribute}
|
||||
style={imageStyle}
|
||||
draggable={false}
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1429,8 +1429,8 @@ export const CacheTypes = {
|
|||
export const ZoomSettings = {
|
||||
DEFAULT_SCALE: 1.75,
|
||||
SCALE_DELTA: 0.25,
|
||||
MIN_SCALE: 0.25,
|
||||
MAX_SCALE: 3.0,
|
||||
MIN_SCALE: 0.1,
|
||||
MAX_SCALE: 5.0,
|
||||
};
|
||||
|
||||
export const DataSpillagePropertyNames = {
|
||||
|
|
|
|||
|
|
@ -931,6 +931,7 @@ export function loadImage(
|
|||
request.open('GET', url, true);
|
||||
request.responseType = 'arraybuffer';
|
||||
request.onload = onLoad;
|
||||
request.onerror = onLoad;
|
||||
request.onprogress = (e) => {
|
||||
if (onProgress) {
|
||||
let total = 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue