wix-style-react
Version:
529 lines (454 loc) • 14.2 kB
JavaScript
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Delete from 'wix-ui-icons-common/Delete';
import Replace from 'wix-ui-icons-common/Replace';
import Download from 'wix-ui-icons-common/Download';
import More from 'wix-ui-icons-common/More';
import StatusIndicator from '../StatusIndicator';
import Loader from '../Loader';
import { st, classes } from './ImageViewer.st.css';
import Tooltip from '../Tooltip';
import IconButton from '../IconButton';
import AddItem from '../AddItem/AddItem';
import Box from '../Box';
import PopoverMenu from '../PopoverMenu';
import classnames from 'classnames';
import { dataHooks } from './constants';
import { TooltipCommonProps } from '../common/PropTypes/TooltipCommon';
class ImageViewer extends Component {
constructor(props) {
super(props);
const { imageUrl } = props;
this.focusNode = React.createRef();
this.state = {
imageLoading: !!imageUrl,
previousImageUrl: undefined,
popoverOpen: false,
};
}
UNSAFE_componentWillReceiveProps(nextProps) {
const { imageUrl: currentImageUrl } = this.props;
const { imageUrl: nextImageUrl } = nextProps;
if (nextImageUrl && currentImageUrl !== nextImageUrl) {
this.setState({
imageLoading: true,
previousImageUrl: currentImageUrl,
});
}
}
_renderAddImage = () => {
const {
onAddImage,
addImageInfo,
tooltipProps = {},
disabled,
} = this.props;
return (
<AddItem
ref={this.focusNode}
onClick={onAddImage}
theme="image"
dataHook={dataHooks.addItem}
disabled={disabled}
tooltipProps={{ ...tooltipProps, content: addImageInfo }}
/>
);
};
/** `display: none` is used to prefetch an image == it fetches the image but doesn't show it */
_renderImageElement = ({
imageUrl,
shouldDisplay,
onLoad,
onError,
key,
dataHook,
}) => {
const dataAttributes = {
'data-hook': dataHook,
'data-image-visible': shouldDisplay,
};
return (
<img
className={classnames([
classes.image,
classes.stretch,
shouldDisplay && classes.imageVisible,
])}
src={imageUrl}
onLoad={onLoad}
onError={onError}
key={key}
{...dataAttributes}
/>
);
};
_resetImageLoading = () => {
this.setState({
imageLoading: false,
});
};
_onImageLoad = e => {
const { onImageLoad } = this.props;
this.setState(
{
imageLoading: false,
},
() => onImageLoad(e),
);
};
_getCurrentAndPreviousImages = () => {
const { imageUrl: currentImageUrl } = this.props;
const { previousImageUrl } = this.state;
return {
currentImageUrl,
previousImageUrl,
};
};
_renderImage = () => {
const { imageLoading } = this.state;
if (!this.props.imageUrl) {
return;
}
const {
currentImageUrl,
previousImageUrl,
} = this._getCurrentAndPreviousImages();
const shouldDisplayContainer = !!(currentImageUrl || previousImageUrl);
const generateKey = (imageName, imageUrl) => `${imageName}-${imageUrl}`;
return (
<div
className={st(classes.imageContainer, {
/** hide container when no image provided, so AddItem behind it can be clickable */
shouldDisplay: shouldDisplayContainer,
})}
data-container-visible={shouldDisplayContainer}
data-hook={dataHooks.imagesContainer}
>
{/** current image */}
{this._renderImageElement({
imageUrl: currentImageUrl,
shouldDisplay: !!currentImageUrl && !imageLoading,
onLoad: this._onImageLoad,
onError: () => {
this._resetImageLoading();
},
dataHook: dataHooks.image,
key: generateKey(dataHooks.image, currentImageUrl),
})}
{/** previous image */}
{this._renderImageElement({
imageUrl: previousImageUrl,
shouldDisplay: imageLoading && !!previousImageUrl,
dataHook: dataHooks.previousImage,
key: generateKey(dataHooks.previousImage, previousImageUrl),
})}
</div>
);
};
_renderUpdateButton = ref => {
const { updateImageInfo, onUpdateImage, tooltipProps } = this.props;
return (
<Tooltip
{...tooltipProps}
timeout={0}
dataHook={dataHooks.updateTooltip}
content={updateImageInfo}
>
<IconButton
ref={ref}
dataHook={dataHooks.update}
onClick={onUpdateImage}
skin="light"
priority="secondary"
>
<Replace />
</IconButton>
</Tooltip>
);
};
_resetPreviousImage = () => this.setState({ previousImageUrl: undefined });
_renderRemoveButton = ref => {
const { removeImageInfo, onRemoveImage, tooltipProps } = this.props;
return (
<Tooltip
{...tooltipProps}
timeout={0}
dataHook={dataHooks.removeTooltip}
content={removeImageInfo}
>
<IconButton
ref={ref}
dataHook={dataHooks.remove}
skin="light"
priority="secondary"
onClick={e => {
this._resetPreviousImage();
onRemoveImage && onRemoveImage(e);
}}
>
<Delete />
</IconButton>
</Tooltip>
);
};
_renderDownloadButton = ref => {
const { downloadImageInfo, onDownloadImage, tooltipProps } = this.props;
return (
<Tooltip
{...tooltipProps}
timeout={0}
dataHook={dataHooks.downloadTooltip}
content={downloadImageInfo}
>
<IconButton
ref={ref}
dataHook={dataHooks.download}
skin="light"
priority="secondary"
onClick={e => {
onDownloadImage && onDownloadImage(e);
}}
>
<Download />
</IconButton>
</Tooltip>
);
};
_hidePopover = () => this.setState({ popoverOpen: false });
_showPopover = () => this.setState({ popoverOpen: true });
_renderMoreButton = () => {
const {
tooltipProps,
moreImageInfo,
downloadImageInfo,
onDownloadImage,
removeImageInfo,
onRemoveImage,
} = this.props;
return (
<PopoverMenu
dataHook={dataHooks.actionsMenu}
onHide={this._hidePopover}
onShow={this._showPopover}
triggerElement={({ toggle }) => (
<Tooltip
{...tooltipProps}
timeout={0}
dataHook={dataHooks.moreTooltip}
content={moreImageInfo}
>
<IconButton
onClick={toggle}
dataHook={dataHooks.more}
skin="light"
priority={this.state.popoverOpen ? 'primary' : 'secondary'}
>
<More />
</IconButton>
</Tooltip>
)}
>
<PopoverMenu.MenuItem
prefixIcon={<Download />}
text={downloadImageInfo}
onClick={onDownloadImage}
/>
<PopoverMenu.MenuItem
prefixIcon={<Delete />}
text={removeImageInfo}
onClick={onRemoveImage}
/>
</PopoverMenu>
);
};
_renderFirstButton = () => {
const {
showUpdateButton,
showRemoveButton,
showDownloadButton,
} = this.props;
if (showUpdateButton) return this._renderUpdateButton(this.focusNode);
if (showDownloadButton) return this._renderDownloadButton(this.focusNode);
if (showRemoveButton) return this._renderRemoveButton(this.focusNode);
return null;
};
_renderSecondButton = () => {
const {
showUpdateButton,
showRemoveButton,
showDownloadButton,
} = this.props;
// All three options - show more button
if (showUpdateButton && showDownloadButton && showRemoveButton) {
return this._renderMoreButton();
}
// Two options - show second button
if (showUpdateButton && showRemoveButton) return this._renderRemoveButton();
if (showUpdateButton && showDownloadButton)
return this._renderDownloadButton();
if (showDownloadButton && showRemoveButton)
return this._renderRemoveButton();
return null;
};
_renderLoader = () => (
<Box
align="center"
verticalAlign="middle"
height="100%"
dataHook={dataHooks.loader}
>
<Loader size="small" />
</Box>
);
_renderButtons = () => {
return (
<div className={classes.buttons}>
{this._renderFirstButton()}
{this._renderSecondButton()}
</div>
);
};
_renderOverlayWith = content => {
const { removeRoundedBorders } = this.props;
const {
currentImageUrl,
previousImageUrl,
} = this._getCurrentAndPreviousImages();
const shouldDisplayOverlay = !!(currentImageUrl || previousImageUrl);
return (
<div
className={st(classes.overlay, {
removeRadius: removeRoundedBorders,
shouldDisplay: shouldDisplayOverlay,
})}
data-remove-radius={removeRoundedBorders}
data-hook={dataHooks.overlay}
>
{content}
<span />
</div>
);
};
/**
* Sets focus on the element
*/
focus = () => {
this.focusNode.current && this.focusNode.current.focus();
};
render() {
const {
width,
height,
disabled,
dataHook,
removeRoundedBorders,
imageUrl,
status,
statusMessage,
className,
} = this.props;
const { imageLoading, previousImageUrl, popoverOpen } = this.state;
const hasImage = !!imageUrl;
const hasNoPreviousImageWhileLoading = imageLoading && !previousImageUrl;
const imageLoaded = hasImage && !imageLoading;
const cssStates = {
disabled,
status: !disabled && status,
removeRadius: removeRoundedBorders,
hasImage,
popoverOpen,
};
const rootDataAttributes = {
'data-disabled': disabled,
'data-image-loaded': imageLoaded,
'data-hook': dataHook,
};
return (
<div
className={st(classes.root, cssStates, className)}
style={{ width, height }}
{...rootDataAttributes}
>
{(hasNoPreviousImageWhileLoading || !hasImage) &&
this._renderAddImage()}
{this._renderImage()}
{this._renderOverlayWith(
imageLoading
? this._renderLoader()
: hasImage && this._renderButtons(),
)}
{/* Status */}
{status && !disabled && (
<div className={classes.statusContainer}>
<StatusIndicator
status={status}
message={statusMessage}
dataHook={dataHooks.errorTooltip}
/>
</div>
)}
</div>
);
}
}
ImageViewer.displayName = 'ImageViewer';
ImageViewer.defaultProps = {
showUpdateButton: true,
showDownloadButton: false,
showRemoveButton: true,
addImageInfo: 'Add Image',
updateImageInfo: 'Update',
downloadImageInfo: 'Download',
removeImageInfo: 'Remove',
moreImageInfo: 'More actions',
onImageLoad: () => ({}),
};
ImageViewer.propTypes = {
/** Applies a data-hook HTML attribute that can be used in the tests. */
dataHook: PropTypes.string,
/** Specifies a CSS class name to be appended to the component’s root element. */
className: PropTypes.string,
/** Links to image asset source (URL). Leave it blank when image is not uploaded yet. */
imageUrl: PropTypes.string,
/** Specifies the status of a viewer. */
status: PropTypes.oneOf(['error', 'warning', 'loading']),
/** Defines the message to display on status icon hover. If not given or empty there will be no tooltip. */
statusMessage: PropTypes.node,
/** Allows to pass all common tooltip props. Check `<Tooltip/>` for a full API. */
tooltipProps: PropTypes.shape(TooltipCommonProps),
/** Specifies whether the update button is visible. */
showUpdateButton: PropTypes.bool,
/** Specifies whether the download button is visible. */
showDownloadButton: PropTypes.bool,
/** Specifies whether the remove button is visible. */
showRemoveButton: PropTypes.bool,
/** Defines a click handler, which is called every time a user clicks on an empty viewer (when no `imageUrl` is provided). */
onAddImage: PropTypes.func,
/** Defines a handler function, which is called every time user clicks on the ‘Update image’ button. */
onUpdateImage: PropTypes.func,
/** Defines a handler function, which is called every time user clicks on the download button. */
onDownloadImage: PropTypes.func,
/** Defines a handler function, which is called every time user clicks on the ‘Remove image’ button. */
onRemoveImage: PropTypes.func,
/** Defines a handler function which is called right after image loads. */
onImageLoad: PropTypes.func,
/** Specifies a message to display in a tooltip when no image is uploaded yet. */
addImageInfo: PropTypes.string,
/** Defines a message to display in a tooltip when ‘Update’ action button is hovered. */
updateImageInfo: PropTypes.string,
/** Defines a message to display in a tooltip when ‘Download’ action button is hovered. */
downloadImageInfo: PropTypes.string,
/** Defines a message to display in a tooltip, when ‘remove’ action button is hovered. */
removeImageInfo: PropTypes.string,
/** Defines a message to display in a tooltip when the ‘More’ action button is hovered. Relevant only when all buttons are visible. */
moreImageInfo: PropTypes.string,
/** Removes default border radius. */
removeRoundedBorders: PropTypes.bool,
/** Sets the width of the viewer box. */
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/** Sets the height of the viewer box. */
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/** Specifies whether the component is disabled. */
disabled: PropTypes.bool,
};
export default ImageViewer;