UNPKG

wix-style-react

Version:
529 lines (454 loc) 14.2 kB
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;