-
Notifications
You must be signed in to change notification settings - Fork 116
/
Copy pathPreview.js
2002 lines (1718 loc) · 67.7 KB
/
Preview.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* eslint-disable import/first */
import EventEmitter from 'events';
import cloneDeep from 'lodash/cloneDeep';
import throttle from 'lodash/throttle';
/* eslint-enable import/first */
import Api from './api';
import Browser from './Browser';
import Logger from './Logger';
import loaderList from './loaders';
import Cache from './Cache';
import PreviewError from './PreviewError';
import PreviewErrorViewer from './viewers/error/PreviewErrorViewer';
import PreviewPerf from './PreviewPerf';
import PreviewUI from './PreviewUI';
import getTokens from './tokens';
import Timer from './Timer';
import {
getProp,
decodeKeydown,
getHeaders,
findScriptLocation,
appendQueryParams,
stripAuthFromString,
isValidFileId,
isBoxWebApp,
convertWatermarkPref,
} from './util';
import {
getURL,
getDownloadURL,
checkPermission,
checkFeature,
checkFileValid,
cacheFile,
uncacheFile,
isWatermarked,
getCachedFile,
normalizeFileVersion,
canDownload,
shouldDownloadWM,
} from './file';
import {
API_HOST,
APP_HOST,
CLASS_NAVIGATION_VISIBILITY,
ERROR_CODE_403_FORBIDDEN_BY_POLICY,
PERMISSION_PREVIEW,
PREVIEW_SCRIPT_NAME,
X_REP_HINT_BASE,
X_REP_HINT_DOC_THUMBNAIL,
X_REP_HINT_IMAGE,
X_REP_HINT_VIDEO_DASH,
X_REP_HINT_VIDEO_MP4,
FILE_OPTION_FILE_VERSION_ID,
} from './constants';
import {
DURATION_METRIC,
ERROR_CODE,
LOAD_METRIC,
PREVIEW_DOWNLOAD_ATTEMPT_EVENT,
PREVIEW_END_EVENT,
PREVIEW_ERROR,
PREVIEW_METRIC,
RENDER_METRIC,
VIEWER_EVENT,
} from './events';
import { getClientLogDetails, getISOTime } from './logUtils';
import { isFeatureEnabled } from './featureChecking';
import './Preview.scss';
const DEFAULT_DISABLED_VIEWERS = ['Office']; // viewers disabled by default
const PREFETCH_COUNT = 4; // number of files to prefetch
const MOUSEMOVE_THROTTLE_MS = 1500; // for showing or hiding the navigation icons
const RETRY_COUNT = 3; // number of times to retry network request for a file
const KEYDOWN_EXCEPTIONS = ['INPUT', 'SELECT', 'TEXTAREA']; // Ignore keydown events on these elements
const LOG_RETRY_TIMEOUT_MS = 500; // retry interval for logging preview event
const LOG_RETRY_COUNT = 3; // number of times to retry logging preview event
const MS_IN_S = 1000; // ms in a sec
const SUPPORT_URL = 'https://support.box.com';
// All preview assets are relative to preview.js. Here we create a location
// object that mimics the window location object and points to where
// preview.js is loaded from by the browser. This needs to be done statically
// outside the class so that location is found while this script is executing
// and not when preview is instantiated, which is too late.
const PREVIEW_LOCATION = findScriptLocation(PREVIEW_SCRIPT_NAME, document.currentScript);
class Preview extends EventEmitter {
/** @property {Api} - Previews Api instance used for XHR calls */
api = new Api();
/** @property {boolean} - Whether preview is open */
open = false;
/** @property {Object} - Analytics that span across preview sessions */
count = {
success: 0, // Counts how many previews have happened overall
error: 0, // Counts how many errors have happened overall
navigation: 0, // Counts how many previews have happened by prev next navigation
};
/** @property {Object} - Current file being previewed */
file = {};
/** @property {Object} - User-specified preview options */
previewOptions = {};
/** @property {Object} - Parsed & computed preview options */
options = {};
/** @property {Object} - Map of disabled viewer names */
disabledViewers = {};
/** @property {Object} - Current viewer instance */
viewer;
/** @property {string[]} - List of file IDs to preview */
collection = [];
/** @property {AssetLoader[]} - List of asset loaders */
loaders = loaderList;
/** @property {Logger} - Logger instance */
logger;
/** @property {number} - Number of times a particular preview has been retried */
retryCount = 0;
/** @property {number} - Number of times a particular logging call cas been retried */
logRetryCount = 0;
/** @property {number} - Reference to preview retry timeout */
retryTimeout;
/** @property {HTMLElement} - Preview DOM container */
container;
/** @property {Function} - Throttled mousemove handler */
throttledMousemoveHandler;
/** @property {Cache} - Preview's cache instance */
cache;
/** @property {PreviewUI} - Preview's UI instance */
ui;
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* [constructor]
*
* @return {Preview} Preview instance
*/
constructor() {
super();
DEFAULT_DISABLED_VIEWERS.forEach(viewerName => {
this.disabledViewers[viewerName] = 1;
});
// All preview assets are relative to preview.js. Here we create a location
// object that mimics the window location object and points to where
// preview.js is loaded from by the browser.
this.location = PREVIEW_LOCATION;
this.cache = new Cache();
this.ui = new PreviewUI();
this.browserInfo = Browser.getBrowserInfo();
// Bind context for callbacks
this.download = this.download.bind(this);
this.print = this.print.bind(this);
this.handleTokenResponse = this.handleTokenResponse.bind(this);
this.handleFileInfoResponse = this.handleFileInfoResponse.bind(this);
this.handleFetchError = this.handleFetchError.bind(this);
this.handleViewerEvents = this.handleViewerEvents.bind(this);
this.handleViewerMetrics = this.handleViewerMetrics.bind(this);
this.triggerError = this.triggerError.bind(this);
this.throttledMousemoveHandler = this.getGlobalMousemoveHandler().bind(this);
this.navigateLeft = this.navigateLeft.bind(this);
this.navigateRight = this.navigateRight.bind(this);
this.keydownHandler = this.keydownHandler.bind(this);
}
/**
* [destructor]
*
* @return {void}
*/
destroy() {
// Log all load metrics
this.emitLoadMetrics();
// Destroy viewer
if (this.viewer && typeof this.viewer.destroy === 'function') {
// Log a preview end event
if (this.file && this.file.id) {
const previewDurationTag = Timer.createTag(this.file.id, DURATION_METRIC);
const previewDurationTimer = Timer.get(previewDurationTag);
Timer.stop(previewDurationTag);
const previewDuration = previewDurationTimer ? previewDurationTimer.elapsed : null;
Timer.reset(previewDurationTag);
this.emitLogEvent(PREVIEW_METRIC, {
event_name: PREVIEW_END_EVENT,
value: previewDuration,
});
}
// Eject http interceptors
this.api.ejectInterceptors();
this.viewer.destroy();
}
this.viewer = undefined;
}
/**
* Primary function for showing a preview of a file.
*
* @public
* @param {string|Object} fileIdOrFile - Box File ID or well-formed Box File object
* @param {string|Function} token - Access token string or generator function
* @param {Object} [options] - Optional preview options
* @return {void}
*/
show(fileIdOrFile, token, options = {}) {
// Save a reference to the options to be re-used later.
// Token should either be a function or a string.
// Token can also be null or undefined for offline use case.
// But it cannot be a random object.
if (token === null || typeof token !== 'object') {
this.previewOptions = { ...options, token };
} else {
throw new Error('Bad access token!');
}
// Initalize performance observers
this.perf = new PreviewPerf();
// Update the optional file navigation collection and caches
// if proper valid file objects were passed in.
this.updateCollection(options.collection);
// Parse the preview options
this.parseOptions(this.previewOptions);
// Load the preview
this.load(fileIdOrFile);
}
/**
* Destroys and hides the preview.
*
* @public
* @return {void}
*/
hide() {
// Indicate preview is closed
this.open = false;
// Destroy the viewer and cleanup preview
this.destroy();
// Destroy pefromance observers
if (this.perf) {
this.perf.destroy();
}
// Clean the UI
this.ui.cleanup();
// Nuke the file
this.file = undefined;
}
/**
* Reloads the current preview. Cleans up existing preview and re-loads from cache.
* Note that reload() will not do anything if either:
* - skipServerUpdate is true (either passed in or defined in preview options) AND cached file is not valid
* - skipServerUpdate is false AND there is no cached file ID
*
* @public
* @param {boolean} skipServerUpdate - Whether or not to update file info from server
* @return {void}
*/
reload(skipServerUpdate) {
// If not passed in, default to Preview option for skipping server update
if (typeof skipServerUpdate === 'undefined') {
// eslint-disable-next-line
skipServerUpdate = this.options.skipServerUpdate;
}
// Reload preview without fetching updated file info from server
if (skipServerUpdate) {
if (!checkFileValid(this.file)) {
return;
}
this.destroy();
this.setupUI();
this.loadViewer();
// Fetch file info from server and reload preview
} else {
if (!this.file.id) {
return;
}
this.load(this.file.id);
}
}
/**
* Updates files to navigate between. Collection can be of files
* or file ids or a mix. We normalize here to file ids for easier
* indexing and cache only the well-formed file objects if provided.
*
* @public
* @param {string[]} [collection] - Updated collection of file or file IDs
* @return {void}
*/
updateCollection(collection) {
const fileOrIds = Array.isArray(collection) ? collection : [];
const files = [];
const fileIds = [];
fileOrIds.forEach(fileOrId => {
if (fileOrId && isValidFileId(fileOrId)) {
// String id found in the collection
fileIds.push(fileOrId.toString());
} else if (fileOrId && typeof fileOrId === 'object' && isValidFileId(fileOrId.id)) {
// Possible well-formed file object found in the collection
const wellFormedFileObj = { ...fileOrId, id: fileOrId.id.toString() };
fileIds.push(wellFormedFileObj.id);
files.push(wellFormedFileObj);
} else {
throw new Error('Bad collection provided!');
}
});
// Update the cache with possibly well-formed file objects.
this.updateFileCache(files);
// Collection always uses string ids for easier indexing.
this.collection = fileIds;
// Since update collection is a public method, it can be
// called anytime to update navigation. If we are showing
// a preview already show or hide the navigation arrows.
if (this.file) {
this.ui.showNavigation(this.file.id, this.collection);
}
}
/**
* Updates the file cache with the provided file metadata. Can be used to
* improve performance if file metadata can be fetched at some point before
* a file is previewed. Note that we only do simple validation that the
* expected properties exist before caching.
*
* @public
* @param {Object[]|Object} [fileMetadata] - Array or single file metadata to cache
* @return {void}
*/
updateFileCache(fileMetadata = []) {
let files = fileMetadata;
if (!Array.isArray(files)) {
files = [fileMetadata];
}
files.forEach(file => {
if (file.watermark_info && file.watermark_info.is_watermarked) {
return;
}
if (checkFileValid(file)) {
cacheFile(this.cache, file);
} else {
const message = '[Preview SDK] Tried to cache invalid file';
// eslint-disable-next-line
console.error(`${message}: `, file);
const err = new PreviewError(ERROR_CODE.INVALID_CACHE_ATTEMPT, message, { file });
this.emitPreviewError(err);
}
});
}
/**
* Returns the current viewer.
*
* @public
* @return {Object|undefined} Current viewer
*/
getCurrentViewer() {
return this.viewer;
}
/**
* Returns the current file being previewed.
*
* @public
* @return {Object|null} Current file
*/
getCurrentFile() {
return this.file;
}
/**
* Returns the current collection of files that preview is aware of.
*
* @public
* @return {Object|null} Current collection
*/
getCurrentCollection() {
return this.collection;
}
/**
* Returns the list of viewers that Preview supports.
*
* @public
* @return {string[]} List of supported viewers
*/
getViewers() {
let viewers = [];
this.loaders.forEach(loader => {
viewers = viewers.concat(loader.getViewers());
});
return viewers;
}
/**
* Disables one or more viewers.
*
* @public
* @param {string|string[]} viewers - destroys the container contents
* @return {void}
*/
disableViewers(viewers) {
if (Array.isArray(viewers)) {
viewers.forEach(viewer => {
this.disabledViewers[viewer] = 1;
});
} else if (viewers) {
this.disabledViewers[viewers] = 1;
}
}
/**
* Enables one or more viewers.
*
* @public
* @param {string|string[]} viewers - destroys the container contents
* @return {void}
*/
enableViewers(viewers) {
if (Array.isArray(viewers)) {
viewers.forEach(viewer => {
delete this.disabledViewers[viewer];
});
} else if (viewers) {
delete this.disabledViewers[viewers];
}
}
/**
* Disables keyboard shortcuts / hotkeys for Preview.
*
* @public
* @return {void}
*/
disableHotkeys() {
this.options.useHotkeys = false;
}
/**
* Enables keyboard shortcuts / hotkeys for Preview.
*
* @public
* @return {void}
*/
enableHotkeys() {
this.options.useHotkeys = true;
}
/**
* Resizes the preview.
*
* @public
* @return {void}
*/
resize() {
if (this.viewer && typeof this.viewer.resize === 'function') {
this.viewer.resize();
}
}
/**
* Checks if the file being previewed supports printing.
*
* @public
* @return {boolean}
*/
canPrint() {
return !!canDownload(this.file, this.options) && checkFeature(this.viewer, 'print');
}
/**
* Prints the file being previewed if the viewer supports printing.
*
* @public
* @return {void}
*/
print() {
if (this.canPrint()) {
this.viewer.print();
}
}
/**
* Downloads the file being previewed.
*
* @public
* @return {void}
*/
download() {
const downloadErrorMsg = __('notification_cannot_download');
const downloadErrorDueToPolicyMsg = __('notification_cannot_download_due_to_policy');
if (!canDownload(this.file, this.options)) {
this.ui.showNotification(downloadErrorMsg);
return;
}
// Make sure to append any optional query params to requests
const { apiHost, queryParams } = this.options;
// If we should download the watermarked representation of the file, generate the representation URL, force
// the correct content disposition, and download
if (shouldDownloadWM(this.file, this.options)) {
const contentUrlTemplate = getProp(this.viewer.getRepresentation(), 'content.url_template');
if (!contentUrlTemplate) {
this.ui.showNotification(downloadErrorMsg);
return;
}
// This allows the browser to download representation content
const params = { response_content_disposition_type: 'attachment', ...queryParams };
const downloadUrl = appendQueryParams(
this.viewer.createContentUrlWithAuthParams(contentUrlTemplate, this.viewer.getAssetPath()),
params,
);
this.api.reachability.downloadWithReachabilityCheck(downloadUrl);
// Otherwise, get the content download URL of the original file and download
} else {
const getDownloadUrl = appendQueryParams(getDownloadURL(this.file.id, apiHost), queryParams);
this.api
.get(getDownloadUrl, { headers: this.getRequestHeaders() })
.then(data => {
const downloadUrl = appendQueryParams(data.download_url, queryParams);
this.api.reachability.downloadWithReachabilityCheck(downloadUrl);
})
.catch(error => {
const code = getProp(error, 'response.data.code');
const msg =
code === ERROR_CODE_403_FORBIDDEN_BY_POLICY ? downloadErrorDueToPolicyMsg : downloadErrorMsg;
this.ui.showNotification(msg);
});
}
this.emitLogEvent(PREVIEW_METRIC, {
event_name: PREVIEW_DOWNLOAD_ATTEMPT_EVENT,
value: this.viewer ? this.viewer.getLoadStatus() : null,
});
}
/**
* Updates the token Preview uses. Passed in parameter can either be a
* string token or token generation function. See tokens.js.
*
* @public
* @param {string|Function} tokenOrTokenFunc - Either an access token or token
* generator function
* @param {boolean} [reload] - Whether or not to reload the current preview
* with the updated token, defaults to true
* @return {void}
*/
updateToken(tokenOrTokenFunc, reload = true) {
this.previewOptions.token = tokenOrTokenFunc;
if (reload) {
this.reload(false); // Fetch file info from server and reload preview with updated token
}
}
/**
* Prefetches a file's viewer assets and content if possible so the browser
* can cache the content and significantly improve preview load time. If
* preload is true, we don't prefetch the file's actual content and instead
* prefetch a lightweight representation, aka preload, of the file so that
* can be shown while the full preview is loading. For example, a document's
* preload representation is a jpg of the first page.
*
* Note that for prefetching to work, the same authentication params (token,
* shared link, shared link password) must be used when prefetching and
* when the actual view happens.
*
* @public
* @param {Object} options - Prefetch options
* @param {string} options.fileId - Box file ID (do not also pass a file version ID)
* @param {string} options.fileVersionId - Box file version ID (do not also pass a file ID)
* @param {string} options.token - Access token
* @param {string} options.sharedLink - Shared link
* @param {string} options.sharedLinkPassword - Shared link password
* @param {boolean} options.preload - Is this prefetch for a preload
* @param {string} token - Access token
* @return {void}
*/
prefetch({ fileId, fileVersionId, token, sharedLink = '', sharedLinkPassword = '', preload = false }) {
let file;
let loader;
let viewer;
// Determining the viewer could throw an error
try {
file = getCachedFile(this.cache, { fileId, fileVersionId });
loader = file ? this.getLoader(file) : null;
viewer = loader ? loader.determineViewer(file, undefined, this.options.viewers) : null;
if (!viewer) {
return;
}
} catch (err) {
const message = `Error prefetching file ID ${fileId} - ${err}`;
// eslint-disable-next-line
console.error(message);
const error = new PreviewError(ERROR_CODE.PREFETCH_FILE, message, {}, err.message);
this.emitPreviewError(error);
return;
}
const options = {
viewer,
file,
token,
// Viewers may ignore this representation when prefetching a preload
representation: loader.determineRepresentation(file, viewer),
};
// If we are prefetching for preload, shared link and password are not set on
// the global this.options for the viewers to use, so we must explicitly pass
// them in
if (preload) {
options.sharedLink = sharedLink;
options.sharedLinkPassword = sharedLinkPassword;
}
const viewerInstance = new viewer.CONSTRUCTOR(this.createViewerOptions(options));
if (typeof viewerInstance.prefetch === 'function') {
viewerInstance.prefetch({
assets: true,
// Prefetch preload if explicitly requested or if viewer has 'preload' option set
preload: preload || !!viewerInstance.getViewerOption('preload'),
// Don't prefetch file's representation content if this is for preload
content: !preload,
});
}
}
/**
* Prefetches static viewer assets for the specified viewers.
*
* @public
* @param {string[]} [viewerNames] - Names of viewers to prefetch, defaults to none
* @return {void}
*/
prefetchViewers(viewerNames = []) {
this.getViewers()
.filter(viewer => viewerNames.indexOf(viewer.NAME) !== -1)
.forEach(viewer => {
const viewerInstance = new viewer.CONSTRUCTOR(
this.createViewerOptions({
viewer,
}),
);
if (typeof viewerInstance.prefetch === 'function') {
viewerInstance.prefetch({
assets: true,
preload: false,
content: false,
});
}
});
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Initial method for loading a preview.
*
* @private
* @param {string|Object} fileIdOrFile - Box File ID or well-formed Box File object
* @return {void}
*/
load(fileIdOrFile) {
// Clean up any existing previews before loading
this.destroy();
// Indicate preview is open
this.open = true;
// Init performance logging
this.logger = new Logger(this.location.locale, this.browserInfo);
// Clear any existing retry timeouts
clearTimeout(this.retryTimeout);
// Save reference to the currently shown file ID and file version ID, if any
const currentFileId = this.file ? this.file.id : undefined;
const currentFileVersionId = this.file && this.file.file_version ? this.file.file_version.id : undefined;
// Save reference to file version we want to load, if any
const fileVersionId = this.getFileOption(fileIdOrFile, FILE_OPTION_FILE_VERSION_ID) || '';
// Check what was passed to preview.show()—string file ID or some file object
if (typeof fileIdOrFile === 'string' || typeof fileIdOrFile === 'number') {
const fileId = fileIdOrFile.toString();
// If we want to load by file version ID, use that as key for cache
const cacheKey = fileVersionId ? { fileVersionId } : { fileId };
// If file info is not cached, create a 'bare' file object that we populate with data from the server later
const bareFile = { id: fileId };
if (fileVersionId) {
bareFile.file_version = {
id: fileVersionId,
};
}
this.file = getCachedFile(this.cache, cacheKey) || bareFile;
// Use well-formed file object if available
} else if (checkFileValid(fileIdOrFile)) {
this.file = fileIdOrFile;
// File is not a well-formed file object but has a file ID and/or file version ID (e.g. Content Explorer)
} else if (fileIdOrFile && typeof fileIdOrFile.id === 'string') {
/* eslint-disable camelcase */
const { id, file_version } = fileIdOrFile;
this.file = { id };
if (file_version) {
this.file.file_version = {
id: file_version.id,
};
}
/* eslint-enable camelcase */
} else {
throw new PreviewError(
ERROR_CODE.BAD_INPUT,
__('error_generic'),
{},
'File is not a well-formed Box File object. See FILE_FIELDS in file.js for a list of required fields.',
);
}
// Start the preview load and render timers when the user starts to perceive preview's load
Timer.start(Timer.createTag(this.file.id, LOAD_METRIC.previewLoadTime));
Timer.start(Timer.createTag(this.file.id, RENDER_METRIC));
// If file version ID is specified, increment retry count if it matches current file version ID
if (fileVersionId) {
if (fileVersionId === currentFileVersionId) {
this.retryCount += 1;
} else {
this.retryCount = 0;
}
// Otherwise, increment retry count if file ID to load matches current file ID
} else if (this.file.id === currentFileId) {
this.retryCount += 1;
// Otherwise, reset retry count
} else {
this.retryCount = 0;
}
// Eject any previous interceptors
this.api.ejectInterceptors();
// Check to see if there are http interceptors and load them
if (this.options.responseInterceptor) {
this.api.addResponseInterceptor(this.options.responseInterceptor);
}
if (this.options.requestInterceptor) {
this.api.addRequestInterceptor(this.options.requestInterceptor);
}
// @TODO: This may not be the best way to detect if we are offline. Make sure this works well if we decided to
// combine Box Elements + Preview. This could potentially break if we have Box Elements fetch the file object
// and pass the well-formed file object directly to the preview library to render.
const isPreviewOffline = typeof fileIdOrFile === 'object' && this.options.skipServerUpdate;
if (isPreviewOffline) {
this.handleTokenResponse({});
} else {
// Fetch access tokens before proceeding
getTokens(this.file.id, this.previewOptions.token)
.then(this.handleTokenResponse)
.catch(this.handleFetchError);
}
}
/**
* Loads preview for the current file given access tokens.
*
* @private
* @param {Object} tokenMap - Map of file ID to access token
* @return {void}
*/
handleTokenResponse(tokenMap) {
// Set the authorization token
this.options.token = tokenMap[this.file.id];
// Do nothing if container is already setup and in the middle of retrying
if (!(this.ui.isSetup() && this.retryCount > 0)) {
this.setupUI();
}
// Load from cache if the current file is valid, otherwise load file info from server
if (checkFileValid(this.file)) {
// Save file in cache. This also adds the 'ORIGINAL' representation. It is required to preview files offline
cacheFile(this.cache, this.file);
this.loadFromCache();
} else {
this.loadFromServer();
}
}
/**
* Sets up preview shell and navigation and starts progress.
*
* @private
* @return {void}
*/
setupUI() {
// Setup the shell
this.container = this.ui.setup(
this.options,
this.keydownHandler,
this.navigateLeft,
this.navigateRight,
this.throttledMousemoveHandler,
);
// Set up the notification
this.ui.setupNotification();
// Update navigation
this.ui.showNavigation(this.file.id, this.collection);
// Setup loading indicator
this.ui.showLoadingIcon(this.file.extension);
this.ui.showLoadingIndicator();
// Start the preview duration timer when the user starts to perceive preview's load
const previewDurationTag = Timer.createTag(this.file.id, DURATION_METRIC);
Timer.start(previewDurationTag);
}
/**
* Parses preview options.
*
* @private
* @param {Object} previewOptions - Options specified by show()
* @return {void}
*/
parseOptions(previewOptions) {
const options = { ...previewOptions };
// Reset all options
this.options = {};
// Container for preview
this.options.container = options.container;
// Shared link URL
this.options.sharedLink = options.sharedLink;
// Shared link password
this.options.sharedLinkPassword = options.sharedLinkPassword;
// Save reference to API host
this.options.apiHost = options.apiHost ? options.apiHost.replace(/\/$/, '') : API_HOST;
// Save reference to the app host
this.options.appHost = options.appHost ? options.appHost.replace(/\/$/, '') : APP_HOST;
// Show or hide the header
this.options.header = options.header || 'light';
// Header element for when using external header outside of the provided container
this.options.headerElement = options.headerElement;
// Custom logo URL
this.options.logoUrl = options.logoUrl || '';
// Allow autofocussing
this.options.autoFocus = options.autoFocus !== false;
// Whether download button should be shown
this.options.showDownload = !!options.showDownload;
// Whether annotations v2 should be shown
this.options.showAnnotations = !!options.showAnnotations;
// Whether the loading indicator should be shown
this.options.showLoading = options.showLoading !== false;
// Whether annotations v4 buttons should be shown in toolbar
this.options.showAnnotationsControls = !!options.showAnnotationsControls;
this.options.showAnnotationsDrawingCreate = !!options.showAnnotationsDrawingCreate;
this.options.enableAnnotationsDiscoverability = !!options.enableAnnotationsDiscoverability;
this.options.enableAnnotationsImageDiscoverability = !!options.enableAnnotationsImageDiscoverability;
// Enable annotations-only control bar when selecting any annotation
this.options.enableAnnotationsOnlyControls = !!options.enableAnnotationsOnlyControls;
// Enable or disable hotkeys
this.options.useHotkeys = options.useHotkeys !== false;
// Custom Box3D application definition
this.options.box3dApplication = options.box3dApplication;
// Custom BoxAnnotations definition
this.options.boxAnnotations = options.boxAnnotations;
// Save the reference to any additional custom options for viewers
this.options.viewers = options.viewers || {};
// Skip load from server and any server updates
this.options.skipServerUpdate = !!options.skipServerUpdate;
// Optional additional query params to append to requests
this.options.queryParams = options.queryParams || {};
// Option to patch AMD module definitions while Preview loads the third party dependencies it expects in the
// browser global scope. Definitions will be re-enabled on the 'assetsloaded' event
this.options.fixDependencies = !!options.fixDependencies || !!options.pauseRequireJS;
// Option to disable 'preview' event log. Use this if you are using Preview in a way that does not constitute
// a full preview, e.g. a content feed. Enabling this option skips the client-side log to the Events API
// (access stats will not be incremented), but content access is still logged server-side for audit purposes
this.options.disableEventLog = !!options.disableEventLog;
// Sets how previews of watermarked files behave.
// 'all' - Forces watermarked previews of supported file types regardless of collaboration or permission level,
// except for `Uploader`, which cannot preview.
// 'any' - The default watermarking behavior in the Box Web Application. If the file type supports
// watermarking, all users except for those collaborated as an `Uploader` will see a watermarked
// preview. If the file type cannot be watermarked, users will see a non-watermarked preview if they
// are at least a `Viewer-Uploader` and no preview otherwise.
// 'none' - Forces non-watermarked previews. If the user is not at least a `Viewer-Uploader`, no preview is
// shown.
this.options.previewWMPref = options.previewWMPref || 'any';
// Whether the download of a watermarked file should be watermarked. This option does not affect non-watermarked
// files. If true, users will be able to download watermarked versions of supported file types as long as they
// have preview permissions (any collaboration role except for `Uploader`).
this.options.downloadWM = !!options.downloadWM;
// Object with ftux experience data that can determine whether specific experiences can show
this.options.experiences = options.experiences || {};
// Options that are applicable to certain file ids
this.options.fileOptions = options.fileOptions || {};
// Option to enable use of thumbnails sidebar for document types
this.options.enableThumbnailsSidebar = !!options.enableThumbnailsSidebar;
// Prefix any user created loaders before our default ones
this.loaders = (options.loaders || []).concat(loaderList);
// Add the request interceptor to the preview instance
this.options.requestInterceptor = options.requestInterceptor;