box-ui-elements
Version:
Box UI Elements
1,337 lines (1,299 loc) • 42.8 kB
JavaScript
const _excluded = ["error"],
_excluded2 = ["advancedContentInsights", "annotatorState", "enableThumbnailsSidebar", "features", "fileOptions", "onAnnotatorEvent", "onAnnotator", "onContentInsightsEventReport", "previewExperiences", "showAnnotationsControls", "token"];
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
/**
*
* @file Content Preview Component
* @author Box
*/
import 'regenerator-runtime/runtime';
import * as React from 'react';
import classNames from 'classnames';
import cloneDeep from 'lodash/cloneDeep';
import flow from 'lodash/flow';
import getProp from 'lodash/get';
import isEqual from 'lodash/isEqual';
import noop from 'lodash/noop';
import omit from 'lodash/omit';
import setProp from 'lodash/set';
import throttle from 'lodash/throttle';
import uniqueid from 'lodash/uniqueId';
import Measure from 'react-measure';
import { withRouter } from 'react-router-dom';
import { decode } from '../../utils/keys';
import makeResponsive from '../common/makeResponsive';
import { withNavRouter } from '../common/nav-router';
import Internationalize from '../common/Internationalize';
import AsyncLoad from '../common/async-load';
// $FlowFixMe TypeScript file
import ThemingStyles from '../common/theming';
// $FlowFixMe TypeScript file
import PreviewContext from './PreviewContext';
import TokenService from '../../utils/TokenService';
import { isInputElement, focus } from '../../utils/dom';
import { getTypedFileId } from '../../utils/file';
import { withAnnotations, withAnnotatorContext } from '../common/annotator-context';
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 { withFeatureConsumer, withFeatureProvider } from '../common/feature-checking';
// $FlowFixMe
import { withBlueprintModernization } from '../common/withBlueprintModernization';
import { EVENT_JS_READY } from '../common/logger/constants';
import ReloadNotification from './ReloadNotification';
import API from '../../api';
import APIContext from '../common/api-context';
import PreviewHeader from './preview-header';
import PreviewMask from './PreviewMask';
import PreviewNavigation from './PreviewNavigation';
import Providers from '../common/Providers';
import { DEFAULT_HOSTNAME_API, DEFAULT_HOSTNAME_APP, DEFAULT_HOSTNAME_STATIC, DEFAULT_PREVIEW_VERSION, DEFAULT_LOCALE, DEFAULT_PATH_STATIC_PREVIEW, CLIENT_NAME_CONTENT_PREVIEW, CLIENT_VERSION, ORIGIN_PREVIEW, ORIGIN_CONTENT_PREVIEW, ERROR_CODE_UNKNOWN } from '../../constants';
// $FlowFixMe TypeScript file
import '../common/fonts.scss';
import '../common/base.scss';
import './ContentPreview.scss';
// Emitted by preview's 'load' event
// Emitted by preview's 'preview_metric' event
const startAtTypes = {
page: 'pages'
};
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}`;
const SCROLL_TO_ANNOTATION_EVENT = 'scrolltoannotation';
mark(MARK_NAME_JS_READY);
const LoadableSidebar = AsyncLoad({
loader: () => import(/* webpackMode: "lazy", webpackChunkName: "content-sidebar" */'../content-sidebar')
});
class ContentPreview extends React.PureComponent {
/**
* @property {number}
*/
/**
* @property {number}
*/
/**
* [constructor]
*
* @return {ContentPreview}
*/
constructor(props) {
super(props);
// Defines a generic type for ContentSidebar, since an import would interfere with code splitting
_defineProperty(this, "contentSidebar", /*#__PURE__*/React.createRef());
_defineProperty(this, "previewBodyRef", /*#__PURE__*/React.createRef());
_defineProperty(this, "previewContextValue", {
previewBodyRef: this.previewBodyRef
});
_defineProperty(this, "previewLibraryLoaded", false);
_defineProperty(this, "initialState", {
canPrint: false,
error: undefined,
isLoading: true,
isReloadNotificationVisible: false,
isThumbnailSidebarOpen: false
});
/**
* Handler for 'preview_error' preview event
*
* @param {PreviewLibraryError} previewError - the error data emitted from preview
* @return {void}
*/
_defineProperty(this, "onPreviewError", _ref => {
let {
error
} = _ref,
rest = _objectWithoutProperties(_ref, _excluded);
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({
isLoading: false,
isThumbnailSidebarOpen: false
});
onError(error, code, _objectSpread(_objectSpread({}, 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}
*/
_defineProperty(this, "onPreviewMetric", previewMetrics => {
const {
logger
} = this.props;
const {
event_name
} = previewMetrics;
let metrics = _objectSpread({}, 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 = _objectSpread(_objectSpread({}, previewMetrics), {}, {
file_info_time: totalFetchFileTime,
value: totalTime
});
}
logger.onPreviewMetric(metrics);
});
/**
* onLoad function for preview
*
* @return {void}
*/
_defineProperty(this, "onPreviewLoad", data => {
const {
onLoad,
collection
} = 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 = _objectSpread(_objectSpread({}, loadData), {}, {
metrics: _objectSpread(_objectSpread({}, loadData.metrics), {}, {
time: totalPreviewMetrics
})
});
}
onLoad(loadData);
this.setState({
isLoading: false
});
this.focusPreview();
if (this.preview && filesToPrefetch.length) {
this.prefetch(filesToPrefetch);
}
this.handleCanPrint();
if (this.dynamicOnPreviewLoadAction) {
this.dynamicOnPreviewLoadAction();
}
});
/**
* Loads preview in the component using the preview library.
*
* @return {void}
*/
_defineProperty(this, "loadPreview", async () => {
const _this$props = this.props,
{
advancedContentInsights,
// will be removed once preview package will be updated to utilize feature flip for ACI
annotatorState: {
activeAnnotationId
} = {},
enableThumbnailsSidebar,
features,
fileOptions,
onAnnotatorEvent,
onAnnotator,
onContentInsightsEventReport,
previewExperiences,
showAnnotationsControls,
token: tokenOrTokenFunction
} = _this$props,
rest = _objectWithoutProperties(_this$props, _excluded2);
const {
file,
selectedVersion,
startAt
} = this.state;
this.previewLibraryLoaded = this.isPreviewLibraryLoaded();
if (!this.previewLibraryLoaded || !file || !tokenOrTokenFunction) {
return;
}
const fileId = this.getFileId(file);
if (fileId !== this.state.currentFileId) {
return;
}
const fileOpts = _objectSpread({}, fileOptions);
const token = typedId => TokenService.getReadTokens(typedId, tokenOrTokenFunction);
if (selectedVersion) {
setProp(fileOpts, [fileId, 'fileVersionId'], selectedVersion.id);
setProp(fileOpts, [fileId, 'currentFileVersionId'], getProp(file, 'file_version.id'));
}
if (activeAnnotationId) {
setProp(fileOpts, [fileId, 'annotations', 'activeId'], activeAnnotationId);
}
if (startAt) {
setProp(fileOpts, [fileId, 'startAt'], startAt);
}
const previewOptions = {
advancedContentInsights,
// will be removed once preview package will be updated to utilize feature flip for ACI
container: `#${this.id} .bcpr-content`,
enableThumbnailsSidebar,
features,
fileOptions: fileOpts,
header: 'none',
headerElement: `#${this.id} .bcpr-PreviewHeader`,
experiences: previewExperiences,
showAnnotations: this.canViewAnnotations(),
showAnnotationsControls,
showDownload: this.canDownload(),
showLoading: false,
showProgress: false,
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
}));
if (showAnnotationsControls) {
this.preview.addListener('annotator_create', onAnnotator);
}
this.preview.updateFileCache([file]);
this.preview.show(file.id, token, _objectSpread(_objectSpread({}, previewOptions), omit(rest, Object.keys(previewOptions))));
if (advancedContentInsights) {
this.preview.addListener('advanced_insights_report', onContentInsightsEventReport);
}
});
/**
* Updates preview file from temporary staged file.
*
* @return {void}
*/
_defineProperty(this, "loadFileFromStage", () => {
if (this.stagedFile) {
this.setState(_objectSpread(_objectSpread({}, this.initialState), {}, {
file: this.stagedFile
}), () => {
this.stagedFile = undefined;
});
}
});
/**
* Removes the reload notification
*
* @return {void}
*/
_defineProperty(this, "closeReloadNotification", () => {
this.setState({
isReloadNotificationVisible: false
});
});
/**
* Tells the preview to resize
*
* @return {void}
*/
_defineProperty(this, "onResize", () => {
if (this.preview && this.preview.getCurrentViewer()) {
this.preview.resize();
}
});
/**
* File fetch success callback
*
* @param {Object} file - Box file
* @return {void}
*/
_defineProperty(this, "fetchFileSuccessCallback", file => {
this.fetchFileEndTime = performance.now();
const {
file: currentFile
} = 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(_objectSpread(_objectSpread({}, 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(_objectSpread(_objectSpread({}, this.initialState), {}, {
isReloadNotificationVisible: true
}));
}
});
/**
* File fetch error callback
*
* @return {void}
*/
_defineProperty(this, "fetchFileErrorCallback", (fileError, code) => {
const {
onError
} = this.props;
const errorCode = fileError.code || code;
const error = {
code: errorCode,
message: fileError.message
};
this.setState({
error,
file: undefined,
isLoading: false
});
onError(fileError, errorCode, {
error: fileError
});
});
/**
* Returns the preview instance
*
* @return {Preview} current instance of preview
*/
_defineProperty(this, "getPreview", () => {
const {
file
} = 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
*/
_defineProperty(this, "getViewer", () => {
const preview = this.getPreview();
const viewer = preview ? preview.getCurrentViewer() : null;
return viewer && viewer.isLoaded() && !viewer.isDestroyed() ? viewer : null;
});
/**
* Shows a preview of the previous file.
*
* @public
* @return {void}
*/
_defineProperty(this, "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}
*/
_defineProperty(this, "navigateRight", () => {
const {
collection
} = 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}
*/
_defineProperty(this, "download", () => {
const {
onDownload
} = this.props;
const {
file
} = this.state;
if (this.preview) {
this.preview.download();
onDownload(cloneDeep(file));
}
});
/**
* Prints file.
*
* @public
* @return {void}
*/
_defineProperty(this, "print", () => {
if (this.preview) {
this.preview.print();
}
});
/**
* Mouse move handler that is throttled and show
* the navigation arrows if applicable.
*
* @return {void}
*/
_defineProperty(this, "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}
*/
_defineProperty(this, "onKeyDown", event => {
const {
useHotkeys
} = 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
*/
_defineProperty(this, "onVersionChange", (version, additionalVersionInfo = {}) => {
const {
onVersionChange
} = this.props;
this.updateVersionToCurrent = additionalVersionInfo.updateVersionToCurrent;
onVersionChange(version, additionalVersionInfo);
this.setState({
selectedVersion: version
});
});
_defineProperty(this, "emitScrollToAnnotation", (id, target) => {
const newViewer = this.getViewer();
// $FlowFixMe - Flow doesn't support optional chaining with method calls
newViewer?.emit(SCROLL_TO_ANNOTATION_EVENT, {
id,
target
});
});
/**
* Handles scrolling to a frame-based annotation by waiting for video player to load first
*
* @param {string} id - The annotation ID
* @param {object} target - The annotation target
* @return {void}
*/
_defineProperty(this, "scrollToFrameAnnotation", (id, target) => {
// $FlowFixMe: querySelector('video') returns an HTMLVideoElement
const videoPlayer = document.querySelector('.bp-media-container video');
if (!videoPlayer) {
this.dynamicOnPreviewLoadAction = null;
return;
}
const videoReadyToScroll = videoPlayer.readyState === 4;
if (videoReadyToScroll) {
this.emitScrollToAnnotation(id, target);
return;
}
const handleLoadedData = () => {
this.emitScrollToAnnotation(id, target);
videoPlayer.removeEventListener('loadeddata', handleLoadedData);
};
videoPlayer.addEventListener('loadeddata', handleLoadedData);
});
_defineProperty(this, "handleAnnotationSelect", ({
file_version,
id,
target
}, deferScrollToOnload = false) => {
const {
location = {}
} = target;
const {
file,
selectedVersion
} = this.state;
const annotationFileVersionId = getProp(file_version, 'id');
const currentFileVersionId = getProp(file, 'file_version.id');
const currentPreviewFileVersionId = getProp(selectedVersion, 'id', currentFileVersionId);
const unit = startAtTypes[location.type];
const viewer = this.getViewer();
if (unit && annotationFileVersionId && annotationFileVersionId !== currentPreviewFileVersionId) {
this.setState({
startAt: {
unit,
value: location.value
}
});
}
if (viewer && !deferScrollToOnload) {
viewer.emit(SCROLL_TO_ANNOTATION_EVENT, {
id,
target
});
} else if (viewer && deferScrollToOnload) {
this.dynamicOnPreviewLoadAction = () => {
if (target?.location?.type === 'frame') {
this.scrollToFrameAnnotation(id, target);
} else {
const newViewer = this.getViewer();
// $FlowFixMe - Flow doesn't support optional chaining with method calls
newViewer?.emit(SCROLL_TO_ANNOTATION_EVENT, {
id,
target
});
}
this.dynamicOnPreviewLoadAction = null;
};
}
});
/**
* Holds the reference the preview container
*
* @return {void}
*/
_defineProperty(this, "containerRef", container => {
this.previewContainer = container;
});
/**
* Fetches a thumbnail for the page given
*
* @return {Promise|null} - promise resolves with the image HTMLElement or null if generation is in progress
*/
_defineProperty(this, "getThumbnail", pageNumber => {
const preview = this.getPreview();
if (preview && preview.viewer) {
return preview.viewer.getThumbnail(pageNumber);
}
return null;
});
const {
apiHost,
cache,
fileId: _fileId,
language,
requestInterceptor,
responseInterceptor,
sharedLink,
sharedLinkPassword,
token: _token
} = props;
this.id = uniqueid('bcpr_');
this.api = new API({
apiHost,
cache,
clientName: CLIENT_NAME_CONTENT_PREVIEW,
language,
requestInterceptor,
responseInterceptor,
sharedLink,
sharedLinkPassword,
token: _token,
version: CLIENT_VERSION
});
this.state = _objectSpread(_objectSpread({}, this.initialState), {}, {
currentFileId: _fileId,
// eslint-disable-next-line react/no-unused-state
prevFileIdProp: _fileId
});
const {
logger: _logger
} = props;
_logger.onReadyMetric({
endMarkName: MARK_NAME_JS_READY
});
}
/**
* Cleanup
*
* @return {void}
*/
componentWillUnmount() {
// Don't destroy the cache while unmounting
this.api.destroy(false);
this.destroyPreview();
}
/**
* Cleans up the preview instance
*/
destroyPreview(shouldReset = true) {
const {
onPreviewDestroy
} = this.props;
if (this.preview) {
this.preview.destroy();
this.preview.removeAllListeners();
this.preview = undefined;
onPreviewDestroy(shouldReset);
}
}
/**
* Destroys api instances with caches
*
* @private
* @return {void}
*/
clearCache() {
this.api.destroy(true);
}
/**
* Once the component mounts, load Preview assets and fetch file info.
*
* @return {void}
*/
componentDidMount() {
this.loadStylesheet();
this.loadScript();
this.fetchFile(this.state.currentFileId);
this.focusPreview();
}
static getDerivedStateFromProps(props, 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, prevState) {
const {
features,
previewExperiences,
token
} = this.props;
const {
features: prevFeatures,
previewExperiences: prevPreviewExperiences,
token: prevToken
} = prevProps;
const {
currentFileId
} = this.state;
const hasFileIdChanged = prevState.currentFileId !== currentFileId;
const hasTokenChanged = prevToken !== token;
const haveAdvancedContentInsightsChanged = !isEqual(prevFeatures?.advancedContentInsights, features?.advancedContentInsights);
const haveExperiencesChanged = prevPreviewExperiences !== previewExperiences;
if (hasFileIdChanged) {
this.destroyPreview();
this.setState({
isLoading: true,
selectedVersion: undefined
});
this.fetchFile(currentFileId);
} else if (this.shouldLoadPreview(prevState)) {
this.destroyPreview(false);
this.setState({
isLoading: true
});
this.loadPreview();
} else if (hasTokenChanged) {
this.updatePreviewToken();
}
if (haveExperiencesChanged && this.preview && this.preview.updateExperiences) {
this.preview.updateExperiences(previewExperiences);
}
if (this.preview?.updateContentInsightsOptions && features?.advancedContentInsights && haveAdvancedContentInsightsChanged) {
this.preview.updateContentInsightsOptions(features?.advancedContentInsights);
}
}
/**
* Updates the access token used by preview library
*
* @param {boolean} shouldReload - true if preview should be reloaded
*/
updatePreviewToken(shouldReload = 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) {
const {
file,
selectedVersion
} = this.state;
const {
file: prevFile,
selectedVersion: prevSelectedVersion
} = 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;
// Check if preview library just became available and we haven't loaded preview yet
// This handles cases where library loads asynchronously after file is already set
if (!this.previewLibraryLoaded && this.isPreviewLibraryLoaded() && file && !this.preview) {
this.previewLibraryLoaded = true;
return true;
}
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) {
const {
staticHost,
staticPath,
language,
previewLibraryVersion
} = this.props;
const path = `${staticPath}/${previewLibraryVersion}/${language}/${asset}`;
const suffix = staticHost.endsWith('/') ? path : `/${path}`;
return `${staticHost}${suffix}`;
}
/**
* Determines if preview assets are loaded
*
* @return {boolean} true if preview is loaded
*/
isPreviewLibraryLoaded() {
return !!global.Box && !!global.Box.Preview;
}
/**
* Loads external css by appending a <link> element
*
* @return {void}
*/
loadStylesheet() {
const {
head
} = document;
const url = 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() {
const {
head
} = document;
const url = 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
} = 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, token) {
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) {
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) {
const {
token
} = this.props;
const typedIds = 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() {
if (!this.fetchFileStartTime || !this.fetchFileEndTime) {
return 0;
}
return Math.round(this.fetchFileEndTime - this.fetchFileStartTime);
}
/**
* 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) {
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;
}
/**
* Returns whether file can be downloaded based on file properties, permissions, and user-defined options.
*
* @return {boolean}
*/
canDownload() {
const {
canDownload
} = this.props;
const {
file
} = 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() {
const {
showAnnotations
} = this.props;
const {
file
} = 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() {
const {
showAnnotations
} = this.props;
const {
file
} = 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);
}
handleCanPrint() {
const preview = this.getPreview();
this.setState({
canPrint: !!preview && (!preview.canPrint || preview.canPrint())
});
}
/**
* 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, successCallback, errorCallback, fetchOptions = {}) {
if (!id) {
return;
}
this.fetchFileStartTime = performance.now();
this.fetchFileEndTime = null;
this.api.getFileAPI().getFile(id, successCallback || this.fetchFileSuccessCallback, errorCallback || this.fetchFileErrorCallback, _objectSpread(_objectSpread({}, fetchOptions), {}, {
fields: PREVIEW_FIELDS_TO_FETCH
}));
}
/**
* Finds the index of current file inside the collection
*
* @return {number} -1 if not indexed
*/
getFileIndex() {
const {
currentFileId
} = this.state;
const {
collection
} = 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) {
const {
collection,
onNavigate
} = 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);
});
}
/**
* Refreshes the content sidebar panel
*
* @return {void}
*/
refreshSidebar() {
const {
current: contentSidebar
} = this.contentSidebar;
if (contentSidebar) {
contentSidebar.refresh();
}
}
/**
* Renders the file preview
*
* @inheritdoc
* @return {Element}
*/
render() {
const {
apiHost,
collection,
token,
language,
messages,
className,
contentAnswersProps,
contentOpenWithProps,
contentSidebarProps,
hasHeader,
hasProviders,
history,
isLarge,
isVeryLarge,
logoUrl,
onClose,
measureRef,
sharedLink,
sharedLinkPassword,
requestInterceptor,
responseInterceptor,
theme
} = this.props;
const {
canPrint,
currentFileId,
error,
file,
isLoading,
isReloadNotificationVisible,
isThumbnailSidebarOpen,
selectedVersion
} = this.state;
const styleClassName = classNames('be bcpr', {
'bcpr-thumbnails-open': isThumbnailSidebarOpen
}, className);
if (!currentFileId) {
return null;
}
const errorCode = getProp(error, 'code');
const currentExtension = getProp(file, 'id') === currentFileId ? getProp(file, 'extension') : '';
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 /*#__PURE__*/React.createElement(Internationalize, {
language: language,
messages: messages
}, /*#__PURE__*/React.createElement(APIContext.Provider, {
value: this.api
}, /*#__PURE__*/React.createElement(PreviewContext.Provider, {
value: this.previewContextValue
}, /*#__PURE__*/React.createElement(Providers, {
hasProviders: hasProviders
}, /*#__PURE__*/React.createElement("div", {
id: this.id,
className: styleClassName,
ref: measureRef,
onKeyDown: this.onKeyDown,
tabIndex: 0
}, /*#__PURE__*/React.createElement(ThemingStyles, {
theme: theme
}), hasHeader && /*#__PURE__*/React.createElement(PreviewHeader, {
file: file,
logoUrl: logoUrl,
token: token,
onClose: onHeaderClose,
onPrint: this.print,
canDownload: this.canDownload(),
canPrint: canPrint,
onDownload: this.download,
contentAnswersProps: contentAnswersProps,
contentOpenWithProps: contentOpenWithProps,
canAnnotate: this.canAnnotate(),
selectedVersion: selectedVersion
}), /*#__PURE__*/React.createElement("div", {
className: "bcpr-body",
ref: this.previewBodyRef
}, /*#__PURE__*/React.createElement("div", {
className: "bcpr-container",
onMouseMove: this.onMouseMove,
ref: this.containerRef
}, file && /*#__PURE__*/React.createElement(Measure, {
bounds: true,
onResize: this.onResize
}, ({
measureRef: previewRef
}) => /*#__PURE__*/React.createElement("div", {
ref: previewRef,
className: "bcpr-content"
})), /*#__PURE__*/React.createElement(PreviewMask, {
errorCode: errorCode,
extension: currentExtension,
isLoading: isLoading
}), /*#__PURE__*/React.createElement(PreviewNavigation, {
collection: collection,
currentIndex: this.getFileIndex(),
onNavigateLeft: this.navigateLeft,
onNavigateRight: this.navigateRight
})), file && /*#__PURE__*/React.createElement(LoadableSidebar, _extends({}, 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,
onAnnotationSelect: this.handleAnnotationSelect,
onVersionChange: this.onVersionChange
}))), isReloadNotificationVisible && /*#__PURE__*/React.createElement(ReloadNotification, {
onClose: this.closeReloadNotification,
onClick: this.loadFileFromStage
}))))));
/* eslint-enable jsx-a11y/no-static-element-interactions */
/* eslint-enable jsx-a11y/no-noninteractive-tabindex */
}
}
_defineProperty(ContentPreview, "defaultProps", {
apiHost: DEFAULT_HOSTNAME_API,
appHost: DEFAULT_HOSTNAME_APP,
autoFocus: false,
canDownload: true,
className: '',
collection: [],
contentAnswersProps: {},
contentOpenWithProps: {},
contentSidebarProps: {},
enableThumbnailsSidebar: false,
hasHeader: false,
language: DEFAULT_LOCALE,
onAnnotator: noop,
onAnnotatorEvent: noop,
onContentInsightsEventReport: noop,
onDownload: noop,
onError: noop,
onLoad: noop,
onNavigate: noop,
onPreviewDestroy: noop,
onVersionChange: noop,
previewLibraryVersion: DEFAULT_PREVIEW_VERSION,
showAnnotations: false,
staticHost: DEFAULT_HOSTNAME_STATIC,
staticPath: DEFAULT_PATH_STATIC_PREVIEW,
useHotkeys: true
});
export { ContentPreview as ContentPreviewComponent };
export default flow([makeResponsive, withAnnotatorContext, withAnnotations, withRouter, withNavRouter, withFeatureConsumer, withFeatureProvider, withBlueprintModernization, withLogger(ORIGIN_CONTENT_PREVIEW), withErrorBoundary(ORIGIN_CONTENT_PREVIEW)])(ContentPreview);
//# sourceMappingURL=ContentPreview.js.map