metadata-based-explorer1
Version:
Box UI Elements
1,236 lines (1,104 loc) • 38.4 kB
JavaScript
/**
* @flow
* @file Content Preview Component
* @author Box
*/
import 'regenerator-runtime/runtime';
import * as React from 'react';
import classNames from 'classnames';
import uniqueid from 'lodash/uniqueId';
import throttle from 'lodash/throttle';
import cloneDeep from 'lodash/cloneDeep';
import omit from 'lodash/omit';
import getProp from 'lodash/get';
import flow from 'lodash/flow';
import noop from 'lodash/noop';
import Measure from 'react-measure';
import type { RouterHistory } from 'react-router-dom';
import { decode } from '../../utils/keys';
import makeResponsive from '../common/makeResponsive';
import Internationalize from '../common/Internationalize';
import AsyncLoad from '../common/async-load';
import TokenService from '../../utils/TokenService';
import { isInputElement, focus } from '../../utils/dom';
import { getTypedFileId } from '../../utils/file';
import { withErrorBoundary } from '../common/error-boundary';
import { withLogger } from '../common/logger';
import { PREVIEW_FIELDS_TO_FETCH } from '../../utils/fields';
import { mark } from '../../utils/performance';
import { withFeatureProvider } from '../common/feature-checking';
import { EVENT_JS_READY } from '../common/logger/constants';
import ReloadNotification from './ReloadNotification';
import API from '../../api';
import PreviewHeader from './preview-header';
import PreviewNavigation from './PreviewNavigation';
import PreviewLoading from './PreviewLoading';
import {
DEFAULT_HOSTNAME_API,
DEFAULT_HOSTNAME_APP,
DEFAULT_HOSTNAME_STATIC,
DEFAULT_PREVIEW_VERSION,
DEFAULT_LOCALE,
DEFAULT_PATH_STATIC_PREVIEW,
CLIENT_NAME_CONTENT_PREVIEW,
ORIGIN_PREVIEW,
ORIGIN_CONTENT_PREVIEW,
ERROR_CODE_UNKNOWN,
} from '../../constants';
import type { ErrorType } from '../common/flowTypes';
import type { VersionChangeCallback } from '../content-sidebar/versions';
import '../common/fonts.scss';
import '../common/base.scss';
import './ContentPreview.scss';
type Props = {
apiHost: string,
appHost: string,
autoFocus: boolean,
cache?: APICache,
canDownload?: boolean,
className: string,
collection: Array<string | BoxItem>,
contentOpenWithProps: ContentOpenWithProps,
contentSidebarProps: ContentSidebarProps,
enableThumbnailsSidebar: boolean,
features?: FeatureConfig,
fileId?: string,
fileOptions?: Object,
getInnerRef: () => ?HTMLElement,
hasHeader?: boolean,
history?: RouterHistory,
isLarge: boolean,
isVeryLarge?: boolean,
language: string,
logoUrl?: string,
measureRef: Function,
messages?: StringMap,
onClose?: Function,
onDownload: Function,
onLoad: Function,
onNavigate: Function,
onVersionChange: VersionChangeCallback,
previewLibraryVersion: string,
requestInterceptor?: Function,
responseInterceptor?: Function,
sharedLink?: string,
sharedLinkPassword?: string,
showAnnotations?: boolean,
staticHost: string,
staticPath: string,
token: Token,
useHotkeys: boolean,
} & ErrorContextProps &
WithLoggerProps;
type State = {
currentFileId?: string,
error?: ErrorType,
file?: BoxItem,
isReloadNotificationVisible: boolean,
isThumbnailSidebarOpen: boolean,
prevFileIdProp?: string, // the previous value of the "fileId" prop. Needed to implement getDerivedStateFromProps
selectedVersion?: BoxItemVersion,
};
// Emitted by preview's 'load' event
type PreviewTimeMetrics = {
conversion: number,
preload?: number,
rendering: number,
total: number,
};
// Emitted by preview's 'preview_metric' event
type PreviewMetrics = {
browser_name: string,
client_version: string,
content_type: string, // Sum of all available load times.
convert_time: number,
download_response_time: number,
error?: Object,
event_name?: string,
extension: string,
file_id: string,
file_info_time: number,
file_version_id: string,
full_document_load_time: number,
locale: string,
logger_session_id: string,
rep_type: string,
timestamp: string,
value: number,
};
type PreviewLibraryError = {
error: ErrorType,
};
const InvalidIdError = new Error('Invalid id for Preview!');
const PREVIEW_LOAD_METRIC_EVENT = 'load';
const MARK_NAME_JS_READY = `${ORIGIN_CONTENT_PREVIEW}_${EVENT_JS_READY}`;
mark(MARK_NAME_JS_READY);
const LoadableSidebar = AsyncLoad({
loader: () => import(/* webpackMode: "lazy", webpackChunkName: "content-sidebar" */ '../content-sidebar'),
});
class ContentPreview extends React.PureComponent<Props, State> {
id: string;
props: Props;
state: State;
preview: any;
api: API;
// Defines a generic type for ContentSidebar, since an import would interfere with code splitting
contentSidebar: { current: null | { refresh: Function } } = React.createRef();
previewContainer: ?HTMLDivElement;
mouseMoveTimeoutID: TimeoutID;
rootElement: HTMLElement;
stagedFile: ?BoxItem;
updateVersionToCurrent: ?() => void;
initialState: State = {
error: undefined,
isReloadNotificationVisible: false,
isThumbnailSidebarOpen: false,
};
static defaultProps = {
apiHost: DEFAULT_HOSTNAME_API,
appHost: DEFAULT_HOSTNAME_APP,
autoFocus: false,
canDownload: true,
className: '',
collection: [],
contentOpenWithProps: {},
contentSidebarProps: {},
enableThumbnailsSidebar: false,
hasHeader: false,
language: DEFAULT_LOCALE,
onDownload: noop,
onError: noop,
onLoad: noop,
onNavigate: noop,
onVersionChange: noop,
previewLibraryVersion: DEFAULT_PREVIEW_VERSION,
showAnnotations: false,
staticHost: DEFAULT_HOSTNAME_STATIC,
staticPath: DEFAULT_PATH_STATIC_PREVIEW,
useHotkeys: true,
};
/**
* @property {number}
*/
fetchFileEndTime: ?number;
/**
* @property {number}
*/
fetchFileStartTime: ?number;
/**
* [constructor]
*
* @return {ContentPreview}
*/
constructor(props: Props) {
super(props);
const {
apiHost,
cache,
fileId,
language,
requestInterceptor,
responseInterceptor,
sharedLink,
sharedLinkPassword,
token,
} = props;
this.id = uniqueid('bcpr_');
this.api = new API({
apiHost,
cache,
clientName: CLIENT_NAME_CONTENT_PREVIEW,
language,
requestInterceptor,
responseInterceptor,
sharedLink,
sharedLinkPassword,
token,
});
this.state = {
...this.initialState,
currentFileId: fileId,
// eslint-disable-next-line react/no-unused-state
prevFileIdProp: fileId,
};
const { logger } = props;
logger.onReadyMetric({
endMarkName: MARK_NAME_JS_READY,
});
}
/**
* Cleanup
*
* @return {void}
*/
componentWillUnmount(): void {
// Don't destroy the cache while unmounting
this.api.destroy(false);
this.destroyPreview();
}
/**
* Cleans up the preview instance
*/
destroyPreview() {
if (this.preview) {
this.preview.destroy();
this.preview.removeAllListeners();
this.preview = undefined;
}
this.setState({ selectedVersion: undefined });
}
/**
* Destroys api instances with caches
*
* @private
* @return {void}
*/
clearCache(): void {
this.api.destroy(true);
}
/**
* Once the component mounts, load Preview assets and fetch file info.
*
* @return {void}
*/
componentDidMount(): void {
this.loadStylesheet();
this.loadScript();
this.fetchFile(this.state.currentFileId);
this.focusPreview();
}
static getDerivedStateFromProps(props: Props, state: State) {
const { fileId } = props;
if (fileId !== state.prevFileIdProp) {
return {
currentFileId: fileId,
prevFileIdProp: fileId,
};
}
return null;
}
/**
* After component updates, load Preview if appropriate.
*
* @return {void}
*/
componentDidUpdate(prevProps: Props, prevState: State): void {
const { token } = this.props;
const { currentFileId } = this.state;
const hasFileIdChanged = prevState.currentFileId !== currentFileId;
const hasTokenChanged = prevProps.token !== token;
if (hasFileIdChanged) {
this.destroyPreview();
this.fetchFile(currentFileId);
} else if (this.shouldLoadPreview(prevState)) {
this.loadPreview();
} else if (hasTokenChanged) {
this.updatePreviewToken();
}
}
/**
* Updates the access token used by preview library
*
* @param {boolean} shouldReload - true if preview should be reloaded
*/
updatePreviewToken(shouldReload: boolean = false) {
if (this.preview) {
this.preview.updateToken(this.props.token, shouldReload);
}
}
/**
* Returns whether or not preview should be loaded.
*
* @param {State} prevState - Previous state
* @return {boolean}
*/
shouldLoadPreview(prevState: State): boolean {
const { file, selectedVersion }: State = this.state;
const { file: prevFile, selectedVersion: prevSelectedVersion }: State = prevState;
const prevSelectedVersionId = getProp(prevSelectedVersion, 'id');
const selectedVersionId = getProp(selectedVersion, 'id');
const prevFileVersionId = getProp(prevFile, 'file_version.id');
const fileVersionId = getProp(file, 'file_version.id');
let loadPreview = false;
if (selectedVersionId !== prevSelectedVersionId) {
const isPreviousCurrent = fileVersionId === prevSelectedVersionId || !prevSelectedVersionId;
const isSelectedCurrent = fileVersionId === selectedVersionId || !selectedVersionId;
// Load preview if the user has selected a non-current version of the file
loadPreview = !isPreviousCurrent || !isSelectedCurrent;
} else if (fileVersionId && prevFileVersionId) {
// Load preview if the file's current version ID has changed
loadPreview = fileVersionId !== prevFileVersionId;
} else {
// Load preview if file object has newly been populated in state
loadPreview = !prevFile && !!file;
}
return loadPreview;
}
/**
* Returns preview asset urls
*
* @return {string} base url
*/
getBasePath(asset: string): string {
const { staticHost, staticPath, language, previewLibraryVersion }: Props = this.props;
const path: string = `${staticPath}/${previewLibraryVersion}/${language}/${asset}`;
const suffix: string = staticHost.endsWith('/') ? path : `/${path}`;
return `${staticHost}${suffix}`;
}
/**
* Determines if preview assets are loaded
*
* @return {boolean} true if preview is loaded
*/
isPreviewLibraryLoaded(): boolean {
return !!global.Box && !!global.Box.Preview;
}
/**
* Loads external css by appending a <link> element
*
* @return {void}
*/
loadStylesheet(): void {
const { head } = document;
const url: string = this.getBasePath('preview.css');
if (!head || head.querySelector(`link[rel="stylesheet"][href="${url}"]`)) {
return;
}
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = url;
head.appendChild(link);
}
/**
* Loads external script by appending a <script> element
*
* @return {void}
*/
loadScript(): void {
const { head } = document;
const url: string = this.getBasePath('preview.js');
if (!head || this.isPreviewLibraryLoaded()) {
return;
}
const previewScript = head.querySelector(`script[src="${url}"]`);
if (previewScript) {
return;
}
const script = document.createElement('script');
script.src = url;
script.addEventListener('load', this.loadPreview);
head.appendChild(script);
}
/**
* Focuses the preview on load.
*
* @return {void}
*/
focusPreview() {
const { autoFocus, getInnerRef }: Props = this.props;
if (autoFocus && !isInputElement(document.activeElement)) {
focus(getInnerRef());
}
}
/**
* Updates preview cache and prefetches a file
*
* @param {BoxItem} file - file to prefetch
* @return {void}
*/
updatePreviewCacheAndPrefetch(file: BoxItem, token: Token): void {
if (!this.preview || !file || !file.id) {
return;
}
this.preview.updateFileCache([file]);
this.preview.prefetch({ fileId: file.id, token });
}
/**
* Gets the file id
*
* @param {string|BoxItem} file - box file or file id
* @return {string} file id
*/
getFileId(file?: string | BoxItem): string {
if (typeof file === 'string') {
return file;
}
if (typeof file === 'object' && !!file.id) {
return file.id;
}
throw InvalidIdError;
}
/**
* Prefetches the next few preview files if any
*
* @param {Array<string|BoxItem>} files - files to prefetch
* @return {void}
*/
async prefetch(files: Array<string | BoxItem>): Promise<void> {
const { token }: Props = this.props;
const typedIds: string[] = files.map(file => getTypedFileId(this.getFileId(file)));
await TokenService.cacheTokens(typedIds, token);
files.forEach(file => {
const fileId = this.getFileId(file);
this.fetchFile(fileId, noop, noop, {
refreshCache: false,
});
});
}
/**
* Calculates the total file fetch time
*
* @return {number} the total fetch time
*/
getTotalFileFetchTime(): number {
if (!this.fetchFileStartTime || !this.fetchFileEndTime) {
return 0;
}
return Math.round(this.fetchFileEndTime - this.fetchFileStartTime);
}
/**
* Handler for 'preview_error' preview event
*
* @param {PreviewLibraryError} previewError - the error data emitted from preview
* @return {void}
*/
onPreviewError = ({ error, ...rest }: PreviewLibraryError): void => {
const { code = ERROR_CODE_UNKNOWN } = error;
const { onError } = this.props;
// In case of error, there should be no thumbnail sidebar to account for
this.setState({ isThumbnailSidebarOpen: false });
onError(
error,
code,
{
...rest,
error,
},
ORIGIN_PREVIEW,
);
};
/**
* Event handler 'preview_metric' which also adds in the file fetch time if it's a load event
*
* @param {Object} previewMetrics - the object emitted by 'preview_metric'
* @return {void}
*/
onPreviewMetric = (previewMetrics: PreviewMetrics): void => {
const { logger } = this.props;
const { event_name } = previewMetrics;
let metrics = {
...previewMetrics,
};
// We need to add in the total file fetch time to the file_info_time and value (total)
// as preview does not do the files call
if (event_name === PREVIEW_LOAD_METRIC_EVENT) {
const totalFetchFileTime = this.getTotalFileFetchTime();
const totalTime = (previewMetrics.value || 0) + totalFetchFileTime;
// If an unnatural load time occurs or is invalid, don't log a load event
if (!totalTime) {
return;
}
metrics = {
...previewMetrics,
file_info_time: totalFetchFileTime,
value: totalTime,
};
}
logger.onPreviewMetric(metrics);
};
/**
* Adds in the file fetch time to the preview metrics
*
* @param {Object} previewTimeMetrics - the preview time metrics
* @return {Object} the preview time metrics merged with the files call time
*/
addFetchFileTimeToPreviewMetrics(previewTimeMetrics: PreviewTimeMetrics): PreviewTimeMetrics {
const totalFetchFileTime = this.getTotalFileFetchTime();
const { rendering, conversion, preload } = previewTimeMetrics;
// We need to add in the total file fetch time to the rendering and total as preview
// does not do the files call. In the case the file is in the process of
// being converted, we need to add to conversion instead of the render
let totalConversion = conversion;
let totalRendering = rendering;
let totalPreload = preload;
if (conversion) {
totalConversion += totalFetchFileTime;
} else {
totalRendering += totalFetchFileTime;
}
if (totalPreload) {
// Preload is optional, depending on file type
totalPreload += totalFetchFileTime;
}
const previewMetrics = {
conversion: totalConversion,
rendering: totalRendering,
total: totalRendering + totalConversion,
preload: totalPreload,
};
return previewMetrics;
}
/**
* onLoad function for preview
*
* @return {void}
*/
onPreviewLoad = (data: Object): void => {
const { onLoad, collection }: Props = this.props;
const currentIndex = this.getFileIndex();
const filesToPrefetch = collection.slice(currentIndex + 1, currentIndex + 5);
const previewTimeMetrics = getProp(data, 'metrics.time');
let loadData = data;
if (previewTimeMetrics) {
const totalPreviewMetrics = this.addFetchFileTimeToPreviewMetrics(previewTimeMetrics);
loadData = {
...loadData,
metrics: {
...loadData.metrics,
time: totalPreviewMetrics,
},
};
}
onLoad(loadData);
this.focusPreview();
if (this.preview && filesToPrefetch.length > 1) {
this.prefetch(filesToPrefetch);
}
};
/**
* Returns whether file can be downloaded based on file properties, permissions, and user-defined options.
*
* @return {boolean}
*/
canDownload(): boolean {
const { canDownload }: Props = this.props;
const { file }: State = this.state;
const isFileDownloadable =
getProp(file, 'permissions.can_download', false) && getProp(file, 'is_download_available', false);
return isFileDownloadable && !!canDownload;
}
/**
* Returns whether file can be annotated based on permissions
*
* @return {boolean}
*/
canAnnotate(): boolean {
const { showAnnotations }: Props = this.props;
const { file }: State = this.state;
const isFileAnnotatable = getProp(file, 'permissions.can_annotate', false);
return !!showAnnotations && isFileAnnotatable;
}
/**
* Returns whether a preview should render annotations based on permissions
*
* @return {boolean}
*/
canViewAnnotations(): boolean {
const { showAnnotations }: Props = this.props;
const { file }: State = this.state;
const hasViewAllPermissions = getProp(file, 'permissions.can_view_annotations_all', false);
const hasViewSelfPermissions = getProp(file, 'permissions.can_view_annotations_self', false);
return !!showAnnotations && (this.canAnnotate() || hasViewAllPermissions || hasViewSelfPermissions);
}
/**
* Loads preview in the component using the preview library.
*
* @return {void}
*/
loadPreview = async (): Promise<void> => {
const { enableThumbnailsSidebar, fileOptions, token: tokenOrTokenFunction, ...rest }: Props = this.props;
const { file, selectedVersion }: State = this.state;
if (!this.isPreviewLibraryLoaded() || !file || !tokenOrTokenFunction) {
return;
}
const fileId = this.getFileId(file);
if (fileId !== this.state.currentFileId) {
return;
}
const fileOpts = { ...fileOptions };
const typedId: string = getTypedFileId(fileId);
const token: TokenLiteral = await TokenService.getReadToken(typedId, tokenOrTokenFunction);
if (selectedVersion) {
fileOpts[fileId] = fileOpts[fileId] || {};
fileOpts[fileId].fileVersionId = selectedVersion.id;
}
const previewOptions = {
container: `#${this.id} .bcpr-content`,
enableThumbnailsSidebar,
fileOptions: fileOpts,
header: 'none',
headerElement: `#${this.id} .bcpr-PreviewHeader`,
showAnnotations: this.canViewAnnotations(),
showDownload: this.canDownload(),
skipServerUpdate: true,
useHotkeys: false,
};
const { Preview } = global.Box;
this.preview = new Preview();
this.preview.addListener('load', this.onPreviewLoad);
this.preview.addListener('preview_error', this.onPreviewError);
this.preview.addListener('preview_metric', this.onPreviewMetric);
this.preview.addListener('thumbnailsOpen', () => this.setState({ isThumbnailSidebarOpen: true }));
this.preview.addListener('thumbnailsClose', () => this.setState({ isThumbnailSidebarOpen: false }));
this.preview.updateFileCache([file]);
this.preview.show(file.id, token, {
...previewOptions,
...omit(rest, Object.keys(previewOptions)),
});
};
/**
* Updates preview file from temporary staged file.
*
* @return {void}
*/
loadFileFromStage = () => {
if (this.stagedFile) {
this.setState({ ...this.initialState, file: this.stagedFile }, () => {
this.stagedFile = undefined;
});
}
};
/**
* Removes the reload notification
*
* @return {void}
*/
closeReloadNotification = () => {
this.setState({ isReloadNotificationVisible: false });
};
/**
* Tells the preview to resize
*
* @return {void}
*/
onResize = (): void => {
if (this.preview && this.preview.getCurrentViewer()) {
this.preview.resize();
}
};
/**
* File fetch success callback
*
* @param {Object} file - Box file
* @return {void}
*/
fetchFileSuccessCallback = (file: BoxItem): void => {
this.fetchFileEndTime = performance.now();
const { file: currentFile }: State = this.state;
const isExistingFile = currentFile ? currentFile.id === file.id : false;
const isWatermarked = getProp(file, 'watermark_info.is_watermarked', false);
// If the file is watermarked or if its a new file, then update the state
// In this case preview should reload without prompting the user
if (isWatermarked || !isExistingFile) {
this.setState({ ...this.initialState, file });
// $FlowFixMe file version and sha1 should exist at this point
} else if (currentFile.file_version.sha1 !== file.file_version.sha1) {
// If we are already prevewing the file that got updated then show the
// user a notification to reload the file only if its sha1 changed
this.stagedFile = file;
this.setState({
...this.initialState,
isReloadNotificationVisible: true,
});
}
};
/**
* File fetch error callback
*
* @return {void}
*/
fetchFileErrorCallback = (fileError: ElementsXhrError, code: string): void => {
const { onError } = this.props;
const errorCode = fileError.code || code;
const error = {
code: errorCode,
message: fileError.message,
};
this.setState({ error, file: undefined });
onError(fileError, errorCode, {
error: fileError,
});
};
/**
* Fetches a file
*
* @param {string} id file id
* @param {Function|void} [successCallback] - Callback after file is fetched
* @param {Function|void} [errorCallback] - Callback after error
* @param {Object|void} [fetchOptions] - Fetch options
* @return {void}
*/
fetchFile(
id: ?string,
successCallback?: Function,
errorCallback?: Function,
fetchOptions?: FetchOptions = {},
): void {
if (!id) {
return;
}
this.fetchFileStartTime = performance.now();
this.fetchFileEndTime = null;
this.api
.getFileAPI()
.getFile(
id,
successCallback || this.fetchFileSuccessCallback,
errorCallback || this.fetchFileErrorCallback,
{
...fetchOptions,
fields: PREVIEW_FIELDS_TO_FETCH,
},
);
}
/**
* Returns the preview instance
*
* @return {Preview} current instance of preview
*/
getPreview = (): any => {
const { file }: State = this.state;
if (!this.preview || !file) {
return null;
}
return this.preview;
};
/**
* Returns the viewer instance being used by preview.
* This will let child components access the viewers.
*
* @return {Viewer} current instance of the preview viewer
*/
getViewer = (): any => {
const preview = this.getPreview();
const viewer = preview ? preview.getCurrentViewer() : null;
return viewer && viewer.isLoaded() && !viewer.isDestroyed() ? viewer : null;
};
/**
* Finds the index of current file inside the collection
*
* @return {number} -1 if not indexed
*/
getFileIndex() {
const { currentFileId }: State = this.state;
const { collection }: Props = this.props;
if (!currentFileId || collection.length < 2) {
return -1;
}
return collection.findIndex(item => {
if (typeof item === 'string') {
return item === currentFileId;
}
return item.id === currentFileId;
});
}
/**
* Shows a preview of a file at the specified index in the current collection.
*
* @public
* @param {number} index - Index of file to preview
* @return {void}
*/
navigateToIndex(index: number) {
const { collection, onNavigate }: Props = this.props;
const { length } = collection;
if (length < 2 || index < 0 || index > length - 1) {
return;
}
const fileOrId = collection[index];
const fileId = typeof fileOrId === 'object' ? fileOrId.id || '' : fileOrId;
this.setState(
{
currentFileId: fileId,
},
() => {
// Execute navigation callback
onNavigate(fileId);
},
);
}
/**
* Shows a preview of the previous file.
*
* @public
* @return {void}
*/
navigateLeft = () => {
const currentIndex = this.getFileIndex();
const newIndex = currentIndex === 0 ? 0 : currentIndex - 1;
if (newIndex !== currentIndex) {
this.navigateToIndex(newIndex);
}
};
/**
* Shows a preview of the next file.
*
* @public
* @return {void}
*/
navigateRight = () => {
const { collection }: Props = this.props;
const currentIndex = this.getFileIndex();
const newIndex = currentIndex === collection.length - 1 ? collection.length - 1 : currentIndex + 1;
if (newIndex !== currentIndex) {
this.navigateToIndex(newIndex);
}
};
/**
* Downloads file.
*
* @public
* @return {void}
*/
download = () => {
const { onDownload }: Props = this.props;
const { file }: State = this.state;
if (this.preview) {
this.preview.download();
onDownload(cloneDeep(file));
}
};
/**
* Prints file.
*
* @public
* @return {void}
*/
print = () => {
if (this.preview) {
this.preview.print();
}
};
/**
* Mouse move handler that is throttled and show
* the navigation arrows if applicable.
*
* @return {void}
*/
onMouseMove = throttle(() => {
const viewer = this.getViewer();
const isPreviewing = !!viewer;
const CLASS_NAVIGATION_VISIBILITY = 'bcpr-nav-is-visible';
clearTimeout(this.mouseMoveTimeoutID);
if (!this.previewContainer) {
return;
}
// Always assume that navigation arrows will be hidden
this.previewContainer.classList.remove(CLASS_NAVIGATION_VISIBILITY);
// Only show it if either we aren't previewing or if we are then the viewer
// is not blocking the show. If we are previewing then the viewer may choose
// to not allow navigation arrows. This is mostly useful for videos since the
// navigation arrows may interfere with the settings menu inside video player.
if (this.previewContainer && (!isPreviewing || viewer.allowNavigationArrows())) {
this.previewContainer.classList.add(CLASS_NAVIGATION_VISIBILITY);
}
this.mouseMoveTimeoutID = setTimeout(() => {
if (this.previewContainer) {
this.previewContainer.classList.remove(CLASS_NAVIGATION_VISIBILITY);
}
}, 1500);
}, 1000);
/**
* Keyboard events
*
* @return {void}
*/
onKeyDown = (event: SyntheticKeyboardEvent<HTMLElement>) => {
const { useHotkeys }: Props = this.props;
if (!useHotkeys) {
return;
}
let consumed = false;
const key = decode(event);
const viewer = this.getViewer();
// If focus was on an input or if the viewer doesn't exist
// then don't bother doing anything further
if (!key || !viewer || isInputElement(event.target)) {
return;
}
if (typeof viewer.onKeydown === 'function') {
consumed = !!viewer.onKeydown(key, event.nativeEvent);
}
if (!consumed) {
switch (key) {
case 'ArrowLeft':
this.navigateLeft();
consumed = true;
break;
case 'ArrowRight':
this.navigateRight();
consumed = true;
break;
default:
// no-op
}
}
if (consumed) {
event.preventDefault();
event.stopPropagation();
}
};
/**
* Handles version change events
*
* @param {string} [version] - The version that is now previewed
* @param {object} [additionalVersionInfo] - extra info about the version
*/
onVersionChange = (version?: BoxItemVersion, additionalVersionInfo?: AdditionalVersionInfo = {}): void => {
const { onVersionChange }: Props = this.props;
this.updateVersionToCurrent = additionalVersionInfo.updateVersionToCurrent;
onVersionChange(version, additionalVersionInfo);
this.setState({
selectedVersion: version,
});
};
/**
* Holds the reference the preview container
*
* @return {void}
*/
containerRef = (container: ?HTMLDivElement) => {
this.previewContainer = container;
};
/**
* Refreshes the content sidebar panel
*
* @return {void}
*/
refreshSidebar(): void {
const { current: contentSidebar } = this.contentSidebar;
if (contentSidebar) {
contentSidebar.refresh();
}
}
/**
* Renders the file preview
*
* @inheritdoc
* @return {Element}
*/
render() {
const {
apiHost,
token,
language,
messages,
className,
contentSidebarProps,
contentOpenWithProps,
hasHeader,
history,
isLarge,
isVeryLarge,
onClose,
measureRef,
sharedLink,
sharedLinkPassword,
requestInterceptor,
responseInterceptor,
}: Props = this.props;
const {
error,
file,
isReloadNotificationVisible,
currentFileId,
isThumbnailSidebarOpen,
selectedVersion,
}: State = this.state;
const { collection }: Props = this.props;
const styleClassName = classNames(
'be bcpr',
{
'bcpr-thumbnails-open': isThumbnailSidebarOpen,
},
className,
);
if (!currentFileId) {
return null;
}
const errorCode = getProp(error, 'code');
const currentVersionId = getProp(file, 'file_version.id');
const selectedVersionId = getProp(selectedVersion, 'id', currentVersionId);
const onHeaderClose = currentVersionId === selectedVersionId ? onClose : this.updateVersionToCurrent;
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
return (
<Internationalize language={language} messages={messages}>
<div id={this.id} className={styleClassName} ref={measureRef} onKeyDown={this.onKeyDown} tabIndex={0}>
{hasHeader && (
<PreviewHeader
file={file}
token={token}
onClose={onHeaderClose}
onPrint={this.print}
canDownload={this.canDownload()}
onDownload={this.download}
contentOpenWithProps={contentOpenWithProps}
canAnnotate={this.canAnnotate()}
selectedVersion={selectedVersion}
/>
)}
<div className="bcpr-body">
<div className="bcpr-container" onMouseMove={this.onMouseMove} ref={this.containerRef}>
{file ? (
<Measure bounds onResize={this.onResize}>
{({ measureRef: previewRef }) => <div ref={previewRef} className="bcpr-content" />}
</Measure>
) : (
<div className="bcpr-loading-wrapper">
<PreviewLoading
errorCode={errorCode}
isLoading={!errorCode}
loadingIndicatorProps={{
size: 'large',
}}
/>
</div>
)}
<PreviewNavigation
collection={collection}
currentIndex={this.getFileIndex()}
onNavigateLeft={this.navigateLeft}
onNavigateRight={this.navigateRight}
/>
</div>
{file && (
<LoadableSidebar
{...contentSidebarProps}
apiHost={apiHost}
token={token}
cache={this.api.getCache()}
fileId={currentFileId}
getPreview={this.getPreview}
getViewer={this.getViewer}
history={history}
isDefaultOpen={isLarge || isVeryLarge}
language={language}
ref={this.contentSidebar}
sharedLink={sharedLink}
sharedLinkPassword={sharedLinkPassword}
requestInterceptor={requestInterceptor}
responseInterceptor={responseInterceptor}
onVersionChange={this.onVersionChange}
/>
)}
</div>
{isReloadNotificationVisible && (
<ReloadNotification onClose={this.closeReloadNotification} onClick={this.loadFileFromStage} />
)}
</div>
</Internationalize>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
/* eslint-enable jsx-a11y/no-noninteractive-tabindex */
}
}
export type ContentPreviewProps = Props;
export { ContentPreview as ContentPreviewComponent };
export default flow([
makeResponsive,
withFeatureProvider,
withLogger(ORIGIN_CONTENT_PREVIEW),
withErrorBoundary(ORIGIN_CONTENT_PREVIEW),
])(ContentPreview);