You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			366 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			366 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			TypeScript
		
	
// tslint:disable:react-a11y-anchors
 | 
						|
 | 
						|
import React from 'react';
 | 
						|
 | 
						|
import classNames from 'classnames';
 | 
						|
import is from '@sindresorhus/is';
 | 
						|
 | 
						|
import * as GoogleChrome from '../util/GoogleChrome';
 | 
						|
import * as MIME from '../types/MIME';
 | 
						|
 | 
						|
import { LocalizerType } from '../types/Util';
 | 
						|
 | 
						|
const Colors = {
 | 
						|
  TEXT_SECONDARY: '#bbb',
 | 
						|
  ICON_SECONDARY: '#ccc',
 | 
						|
};
 | 
						|
 | 
						|
const colorSVG = (url: string, color: string) => {
 | 
						|
  return {
 | 
						|
    WebkitMask: `url(${url}) no-repeat center`,
 | 
						|
    WebkitMaskSize: '100%',
 | 
						|
    backgroundColor: color,
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
interface Props {
 | 
						|
  close: () => void;
 | 
						|
  contentType: MIME.MIMEType | undefined;
 | 
						|
  i18n: LocalizerType;
 | 
						|
  objectURL: string;
 | 
						|
  caption?: string;
 | 
						|
  onNext?: () => void;
 | 
						|
  onPrevious?: () => void;
 | 
						|
  onSave?: () => void;
 | 
						|
}
 | 
						|
 | 
						|
const CONTROLS_WIDTH = 50;
 | 
						|
const CONTROLS_SPACING = 10;
 | 
						|
 | 
						|
const styles = {
 | 
						|
  container: {
 | 
						|
    display: 'flex',
 | 
						|
    flexDirection: 'column',
 | 
						|
    position: 'absolute',
 | 
						|
    left: 0,
 | 
						|
    right: 0,
 | 
						|
    top: 0,
 | 
						|
    bottom: 0,
 | 
						|
    backgroundColor: 'rgba(0, 0, 0, 0.9)',
 | 
						|
  } as React.CSSProperties,
 | 
						|
  mainContainer: {
 | 
						|
    display: 'flex',
 | 
						|
    flexDirection: 'row',
 | 
						|
    flexGrow: 1,
 | 
						|
    paddingTop: 40,
 | 
						|
    paddingLeft: 40,
 | 
						|
    paddingRight: 40,
 | 
						|
    paddingBottom: 0,
 | 
						|
  } as React.CSSProperties,
 | 
						|
  objectContainer: {
 | 
						|
    position: 'relative',
 | 
						|
    flexGrow: 1,
 | 
						|
    display: 'inline-flex',
 | 
						|
    justifyContent: 'center',
 | 
						|
  } as React.CSSProperties,
 | 
						|
  object: {
 | 
						|
    flexGrow: 1,
 | 
						|
    flexShrink: 0,
 | 
						|
    maxWidth: '100%',
 | 
						|
    maxHeight: '100%',
 | 
						|
    objectFit: 'contain',
 | 
						|
  } as React.CSSProperties,
 | 
						|
  caption: {
 | 
						|
    position: 'absolute',
 | 
						|
    bottom: 0,
 | 
						|
    left: 0,
 | 
						|
    right: 0,
 | 
						|
    textAlign: 'center',
 | 
						|
    color: 'white',
 | 
						|
    padding: '1em',
 | 
						|
    paddingLeft: '3em',
 | 
						|
    paddingRight: '3em',
 | 
						|
    backgroundColor: 'rgba(192, 192, 192, .20)',
 | 
						|
  } as React.CSSProperties,
 | 
						|
  controlsOffsetPlaceholder: {
 | 
						|
    width: CONTROLS_WIDTH,
 | 
						|
    marginRight: CONTROLS_SPACING,
 | 
						|
    flexShrink: 0,
 | 
						|
  },
 | 
						|
  controls: {
 | 
						|
    width: CONTROLS_WIDTH,
 | 
						|
    flexShrink: 0,
 | 
						|
    display: 'flex',
 | 
						|
    flexDirection: 'column',
 | 
						|
    marginLeft: CONTROLS_SPACING,
 | 
						|
  } as React.CSSProperties,
 | 
						|
  navigationContainer: {
 | 
						|
    flexShrink: 0,
 | 
						|
    display: 'flex',
 | 
						|
    flexDirection: 'row',
 | 
						|
    justifyContent: 'center',
 | 
						|
    padding: 10,
 | 
						|
  } as React.CSSProperties,
 | 
						|
  saveButton: {
 | 
						|
    marginTop: 10,
 | 
						|
  },
 | 
						|
  iconButtonPlaceholder: {
 | 
						|
    // Dimensions match `.iconButton`:
 | 
						|
    display: 'inline-block',
 | 
						|
    width: 50,
 | 
						|
    height: 50,
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
interface IconButtonProps {
 | 
						|
  onClick?: () => void;
 | 
						|
  style?: React.CSSProperties;
 | 
						|
  type: 'save' | 'close' | 'previous' | 'next';
 | 
						|
}
 | 
						|
 | 
						|
const IconButton = ({ onClick, style, type }: IconButtonProps) => {
 | 
						|
  const clickHandler = (event: React.MouseEvent<HTMLAnchorElement>): void => {
 | 
						|
    event.preventDefault();
 | 
						|
    if (!onClick) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    onClick();
 | 
						|
  };
 | 
						|
 | 
						|
  return (
 | 
						|
    <a
 | 
						|
      href="#"
 | 
						|
      onClick={clickHandler}
 | 
						|
      className={classNames('iconButton', type)}
 | 
						|
      role="button"
 | 
						|
      style={style}
 | 
						|
    />
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
const IconButtonPlaceholder = () => (
 | 
						|
  <div style={styles.iconButtonPlaceholder} />
 | 
						|
);
 | 
						|
 | 
						|
const Icon = ({
 | 
						|
  onClick,
 | 
						|
  url,
 | 
						|
}: {
 | 
						|
  onClick?: (
 | 
						|
    event: React.MouseEvent<HTMLImageElement | HTMLDivElement>
 | 
						|
  ) => void;
 | 
						|
  url: string;
 | 
						|
}) => (
 | 
						|
  <div
 | 
						|
    style={{
 | 
						|
      ...styles.object,
 | 
						|
      ...colorSVG(url, Colors.ICON_SECONDARY),
 | 
						|
      maxWidth: 200,
 | 
						|
    }}
 | 
						|
    onClick={onClick}
 | 
						|
    role="button"
 | 
						|
  />
 | 
						|
);
 | 
						|
 | 
						|
export class Lightbox extends React.Component<Props> {
 | 
						|
  private readonly containerRef: React.RefObject<HTMLDivElement>;
 | 
						|
  private readonly videoRef: React.RefObject<HTMLVideoElement>;
 | 
						|
  private readonly playVideoBound: () => void;
 | 
						|
 | 
						|
  constructor(props: Props) {
 | 
						|
    super(props);
 | 
						|
 | 
						|
    this.playVideoBound = this.playVideo.bind(this);
 | 
						|
 | 
						|
    this.videoRef = React.createRef();
 | 
						|
    this.containerRef = React.createRef();
 | 
						|
  }
 | 
						|
 | 
						|
  public componentDidMount() {
 | 
						|
    const useCapture = true;
 | 
						|
    document.addEventListener('keyup', this.onKeyUp, useCapture);
 | 
						|
 | 
						|
    this.playVideo();
 | 
						|
  }
 | 
						|
 | 
						|
  public componentWillUnmount() {
 | 
						|
    const useCapture = true;
 | 
						|
    document.removeEventListener('keyup', this.onKeyUp, useCapture);
 | 
						|
  }
 | 
						|
 | 
						|
  public playVideo() {
 | 
						|
    if (!this.videoRef) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    const { current } = this.videoRef;
 | 
						|
    if (!current) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (current.paused) {
 | 
						|
      // tslint:disable-next-line no-floating-promises
 | 
						|
      current.play();
 | 
						|
    } else {
 | 
						|
      current.pause();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  public render() {
 | 
						|
    const {
 | 
						|
      caption,
 | 
						|
      contentType,
 | 
						|
      objectURL,
 | 
						|
      onNext,
 | 
						|
      onPrevious,
 | 
						|
      onSave,
 | 
						|
      i18n,
 | 
						|
    } = this.props;
 | 
						|
 | 
						|
    return (
 | 
						|
      <div
 | 
						|
        style={styles.container}
 | 
						|
        onClick={this.onContainerClick}
 | 
						|
        ref={this.containerRef}
 | 
						|
        role="dialog"
 | 
						|
      >
 | 
						|
        <div style={styles.mainContainer}>
 | 
						|
          <div style={styles.controlsOffsetPlaceholder} />
 | 
						|
          <div style={styles.objectContainer}>
 | 
						|
            {!is.undefined(contentType)
 | 
						|
              ? this.renderObject({ objectURL, contentType, i18n })
 | 
						|
              : null}
 | 
						|
            {caption ? <div style={styles.caption}>{caption}</div> : null}
 | 
						|
          </div>
 | 
						|
          <div style={styles.controls}>
 | 
						|
            <IconButton type="close" onClick={this.onClose} />
 | 
						|
            {onSave ? (
 | 
						|
              <IconButton
 | 
						|
                type="save"
 | 
						|
                onClick={onSave}
 | 
						|
                style={styles.saveButton}
 | 
						|
              />
 | 
						|
            ) : null}
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
        <div style={styles.navigationContainer}>
 | 
						|
          {onPrevious ? (
 | 
						|
            <IconButton type="previous" onClick={onPrevious} />
 | 
						|
          ) : (
 | 
						|
            <IconButtonPlaceholder />
 | 
						|
          )}
 | 
						|
          {onNext ? (
 | 
						|
            <IconButton type="next" onClick={onNext} />
 | 
						|
          ) : (
 | 
						|
            <IconButtonPlaceholder />
 | 
						|
          )}
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  private readonly renderObject = ({
 | 
						|
    objectURL,
 | 
						|
    contentType,
 | 
						|
    i18n,
 | 
						|
  }: {
 | 
						|
    objectURL: string;
 | 
						|
    contentType: MIME.MIMEType;
 | 
						|
    i18n: LocalizerType;
 | 
						|
  }) => {
 | 
						|
    const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
 | 
						|
    if (isImageTypeSupported) {
 | 
						|
      return (
 | 
						|
        <img
 | 
						|
          alt={i18n('lightboxImageAlt')}
 | 
						|
          style={styles.object}
 | 
						|
          src={objectURL}
 | 
						|
          onClick={this.onObjectClick}
 | 
						|
        />
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
 | 
						|
    if (isVideoTypeSupported) {
 | 
						|
      return (
 | 
						|
        <video
 | 
						|
          role="button"
 | 
						|
          ref={this.videoRef}
 | 
						|
          onClick={this.playVideoBound}
 | 
						|
          controls={true}
 | 
						|
          style={styles.object}
 | 
						|
          key={objectURL}
 | 
						|
        >
 | 
						|
          <source src={objectURL} />
 | 
						|
        </video>
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    const isUnsupportedImageType =
 | 
						|
      !isImageTypeSupported && MIME.isImage(contentType);
 | 
						|
    const isUnsupportedVideoType =
 | 
						|
      !isVideoTypeSupported && MIME.isVideo(contentType);
 | 
						|
    if (isUnsupportedImageType || isUnsupportedVideoType) {
 | 
						|
      const iconUrl = isUnsupportedVideoType
 | 
						|
        ? 'images/video.svg'
 | 
						|
        : 'images/image.svg';
 | 
						|
 | 
						|
      return <Icon url={iconUrl} onClick={this.onObjectClick} />;
 | 
						|
    }
 | 
						|
 | 
						|
    // tslint:disable-next-line no-console
 | 
						|
    console.log('Lightbox: Unexpected content type', { contentType });
 | 
						|
 | 
						|
    return <Icon onClick={this.onObjectClick} url="images/file.svg" />;
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly onClose = () => {
 | 
						|
    const { close } = this.props;
 | 
						|
    if (!close) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    close();
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly onKeyUp = (event: KeyboardEvent) => {
 | 
						|
    const { onNext, onPrevious } = this.props;
 | 
						|
    switch (event.key) {
 | 
						|
      case 'Escape':
 | 
						|
        this.onClose();
 | 
						|
        break;
 | 
						|
 | 
						|
      case 'ArrowLeft':
 | 
						|
        if (onPrevious) {
 | 
						|
          onPrevious();
 | 
						|
        }
 | 
						|
        break;
 | 
						|
 | 
						|
      case 'ArrowRight':
 | 
						|
        if (onNext) {
 | 
						|
          onNext();
 | 
						|
        }
 | 
						|
        break;
 | 
						|
 | 
						|
      default:
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly onContainerClick = (
 | 
						|
    event: React.MouseEvent<HTMLDivElement>
 | 
						|
  ) => {
 | 
						|
    if (this.containerRef && event.target !== this.containerRef.current) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.onClose();
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly onObjectClick = (
 | 
						|
    event: React.MouseEvent<HTMLImageElement | HTMLDivElement>
 | 
						|
  ) => {
 | 
						|
    event.stopPropagation();
 | 
						|
    this.onClose();
 | 
						|
  };
 | 
						|
}
 |