From ffac1bab843771b3ab2d61b56874e7c2f1cd95e8 Mon Sep 17 00:00:00 2001 From: Jared Stoffan Date: Thu, 8 Apr 2021 12:28:06 -0700 Subject: [PATCH] feat(loading): Show ghost state for document preloaders and pages --- src/lib/_loading.scss | 42 ------------------- src/lib/constants.js | 3 +- src/lib/viewers/BaseViewer.js | 3 -- src/lib/viewers/doc/DocPreloader.js | 38 ++++++++--------- src/lib/viewers/doc/Document.scss | 36 ++++++++++------ src/lib/viewers/doc/Presentation.scss | 30 +++++++++---- src/lib/viewers/doc/PresentationPreloader.js | 19 ++------- .../doc/__tests__/DocPreloader-test.js | 28 +++++++------ .../__tests__/PresentationPreloader-test.js | 22 +++------- src/lib/viewers/doc/_docBase.scss | 16 ++++--- src/lib/viewers/doc/_loading.scss | 35 ++++++++++++++++ 11 files changed, 134 insertions(+), 138 deletions(-) create mode 100644 src/lib/viewers/doc/_loading.scss diff --git a/src/lib/_loading.scss b/src/lib/_loading.scss index 0e78248218..1fc6f8d14b 100644 --- a/src/lib/_loading.scss +++ b/src/lib/_loading.scss @@ -1,12 +1,5 @@ @import 'boxuiVariables'; -$spinner-size: 15px; - -@mixin spinner() { - background: url('icons/loading.gif') center no-repeat; - background-size: $spinner-size $spinner-size; -} - @keyframes box-crawler { 0%, 80%, @@ -21,17 +14,6 @@ $spinner-size: 15px; } } -@keyframes fadeIn { - 0%, - 50% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - .bp-loading-wrapper { position: absolute; top: 0; @@ -100,27 +82,3 @@ $spinner-size: 15px; animation-delay: .2s; } } - -.bp .bp-doc.bp-doc-document, -.bp .bp-doc.bp-doc-presentation { - // Overrides PDF.js loading spinner - .pdfViewer .page .loadingIcon { - @include spinner; - } -} - -.bp-document-preload-wrapper, -.bp-presentation-preload-wrapper { - .bp-preload-spinner { - @include spinner; - - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - width: $spinner-size; - height: $spinner-size; - margin: auto; - } -} diff --git a/src/lib/constants.js b/src/lib/constants.js index c2268906b6..5414fc8b31 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -27,9 +27,10 @@ export const CLASS_BOX_PREVIEW_MOBILE = 'bp-is-mobile'; export const CLASS_BOX_PREVIEW_OVERLAY = 'bp-overlay'; export const CLASS_BOX_PREVIEW_OVERLAY_WRAPPER = 'bp-overlay-wrapper'; export const CLASS_BOX_PREVIEW_PRELOAD = 'bp-preload'; +export const CLASS_BOX_PREVIEW_PRELOAD_BACKGROUND = 'bp-preload-background'; export const CLASS_BOX_PREVIEW_PRELOAD_CONTENT = 'bp-preload-content'; export const CLASS_BOX_PREVIEW_PRELOAD_OVERLAY = 'bp-preload-overlay'; -export const CLASS_BOX_PREVIEW_PRELOAD_SPINNER = 'bp-preload-spinner'; +export const CLASS_BOX_PREVIEW_PRELOAD_PLACEHOLDER = 'bp-preload-placeholder'; export const CLASS_BOX_PREVIEW_PRELOAD_WRAPPER_DOCUMENT = 'bp-document-preload-wrapper'; export const CLASS_BOX_PREVIEW_PRELOAD_WRAPPER_PRESENTATION = 'bp-presentation-preload-wrapper'; export const CLASS_BOX_PREVIEW_NOTIFICATION = 'bp-notification'; diff --git a/src/lib/viewers/BaseViewer.js b/src/lib/viewers/BaseViewer.js index ea4b677d26..ce40232bdf 100644 --- a/src/lib/viewers/BaseViewer.js +++ b/src/lib/viewers/BaseViewer.js @@ -88,9 +88,6 @@ class BaseViewer extends EventEmitter { /** @property {number} - Zoom scale, if zoomed */ scale = 1; - /** @property {string} - Viewer-specific file loading icon */ - fileLoadingIcon; - /** @property {Object} - Viewer options */ options; diff --git a/src/lib/viewers/doc/DocPreloader.js b/src/lib/viewers/doc/DocPreloader.js index 0fe17f333f..6756cd451e 100644 --- a/src/lib/viewers/doc/DocPreloader.js +++ b/src/lib/viewers/doc/DocPreloader.js @@ -2,17 +2,18 @@ import EventEmitter from 'events'; import Api from '../../api'; import { CLASS_BOX_PREVIEW_PRELOAD, + CLASS_BOX_PREVIEW_PRELOAD_BACKGROUND, CLASS_BOX_PREVIEW_PRELOAD_CONTENT, CLASS_BOX_PREVIEW_PRELOAD_OVERLAY, - CLASS_BOX_PREVIEW_PRELOAD_SPINNER, + CLASS_BOX_PREVIEW_PRELOAD_PLACEHOLDER, CLASS_BOX_PREVIEW_PRELOAD_WRAPPER_DOCUMENT, CLASS_INVISIBLE, CLASS_IS_TRANSPARENT, CLASS_PREVIEW_LOADED, PDFJS_CSS_UNITS, + PDFJS_HEIGHT_PADDING_PX, PDFJS_MAX_AUTO_SCALE, PDFJS_WIDTH_PADDING_PX, - PDFJS_HEIGHT_PADDING_PX, } from '../../constants'; import { setDimensions, handleRepresentationBlobFetch } from '../../util'; @@ -24,8 +25,6 @@ const NUM_PAGES_MAX = 500; // Don't show more than 500 placeholder pages const ACCEPTABLE_RATIO_DIFFERENCE = 0.025; // Acceptable difference in ratio of PDF dimensions to image dimensions -const SPINNER_HTML = `
`; - class DocPreloader extends EventEmitter { /** @property {Api} - Api layer used for XHR calls */ api = new Api(); @@ -39,12 +38,12 @@ class DocPreloader extends EventEmitter { /** @property {HTMLElement} - Maximum auto-zoom scale */ maxZoomScale = PDFJS_MAX_AUTO_SCALE; - /** @property {HTMLElement} - Preload overlay element */ - overlayEl; - /** @property {Object} - The EXIF data for the PDF */ pdfData; + /** @property {HTMLElement} - Preload placeholder element */ + placeholderEl; + /** @property {HTMLElement} - Preload container element */ preloadEl; @@ -101,17 +100,18 @@ class DocPreloader extends EventEmitter { this.wrapperEl.className = this.wrapperClassName; this.wrapperEl.innerHTML = `
- -
- ${SPINNER_HTML} +
+
+ +
`.trim(); this.containerEl.appendChild(this.wrapperEl); + this.placeholderEl = this.wrapperEl.querySelector(`.${CLASS_BOX_PREVIEW_PRELOAD_PLACEHOLDER}`); this.preloadEl = this.wrapperEl.querySelector(`.${CLASS_BOX_PREVIEW_PRELOAD}`); - this.imageEl = this.preloadEl.querySelector(`img.${CLASS_BOX_PREVIEW_PRELOAD_CONTENT}`); - this.overlayEl = this.preloadEl.querySelector(`.${CLASS_BOX_PREVIEW_PRELOAD_OVERLAY}`); + this.imageEl = this.preloadEl.querySelector(`.${CLASS_BOX_PREVIEW_PRELOAD_CONTENT}`); this.bindDOMListeners(); }); } @@ -129,22 +129,18 @@ class DocPreloader extends EventEmitter { return; } - // Set image and overlay dimensions - setDimensions(this.imageEl, scaledWidth, scaledHeight); - setDimensions(this.overlayEl, scaledWidth, scaledHeight); + // Set initial placeholder dimensions + setDimensions(this.placeholderEl, scaledWidth, scaledHeight); // Add and scale correct number of placeholder elements for (let i = 0; i < numPages - 1; i += 1) { const placeholderEl = document.createElement('div'); - placeholderEl.className = CLASS_BOX_PREVIEW_PRELOAD_CONTENT; - placeholderEl.innerHTML = SPINNER_HTML; + placeholderEl.className = CLASS_BOX_PREVIEW_PRELOAD_PLACEHOLDER; + placeholderEl.innerHTML = `
`; setDimensions(placeholderEl, scaledWidth, scaledHeight); this.preloadEl.appendChild(placeholderEl); } - // Hide the preview-level loading indicator - this.previewUI.hideLoadingIndicator(); - // Show preload element after content is properly sized this.preloadEl.classList.remove(CLASS_INVISIBLE); @@ -292,7 +288,7 @@ class DocPreloader extends EventEmitter { const { scaledWidth, scaledHeight } = dimensionData; // Scale preload and placeholder elements - const preloadEls = this.preloadEl.getElementsByClassName(CLASS_BOX_PREVIEW_PRELOAD_CONTENT); + const preloadEls = this.preloadEl.getElementsByClassName(CLASS_BOX_PREVIEW_PRELOAD_PLACEHOLDER); for (let i = 0; i < preloadEls.length; i += 1) { setDimensions(preloadEls[i], scaledWidth, scaledHeight); } diff --git a/src/lib/viewers/doc/Document.scss b/src/lib/viewers/doc/Document.scss index 780ae41d32..38eb9176fe 100644 --- a/src/lib/viewers/doc/Document.scss +++ b/src/lib/viewers/doc/Document.scss @@ -34,21 +34,31 @@ } } - // Position preload content as if they were blank loading pages + .bp-preload-background, + .bp-preload-content, + .bp-preload-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + + .bp-preload-background { + @include bp-Ghost; + } + .bp-preload-content { + width: 100%; + } + + .bp-preload-overlay { + background-color: rgba(255, 255, 255, .4); + } + + .bp-preload-placeholder { position: relative; - display: block; margin: 15px auto 30px; - padding-left: 1px; // Slight padding to help image text align with real text - background-color: $white; - - &.bp-preload-overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-color: rgba(255, 255, 255, .4); - } + overflow: hidden; } } diff --git a/src/lib/viewers/doc/Presentation.scss b/src/lib/viewers/doc/Presentation.scss index 30f030477f..d46f61def3 100644 --- a/src/lib/viewers/doc/Presentation.scss +++ b/src/lib/viewers/doc/Presentation.scss @@ -53,19 +53,31 @@ } } - // Absolutely center preload content - .bp-preload-content { + .bp-preload-background, + .bp-preload-content, + .bp-preload-overlay, + .bp-preload-placeholder { position: absolute; top: 0; right: 0; bottom: 0; - left: 1px; // Slight padding to help image text align with real text - display: block; - margin: auto; - background-color: $white; + left: 0; + } - &.bp-preload-overlay { - background-color: rgba(255, 255, 255, .4); - } + .bp-preload-background { + @include bp-Ghost; + } + + .bp-preload-content { + width: 100%; + } + + .bp-preload-overlay { + background-color: rgba(255, 255, 255, .4); + } + + .bp-preload-placeholder { + margin: auto; + overflow: hidden; } } diff --git a/src/lib/viewers/doc/PresentationPreloader.js b/src/lib/viewers/doc/PresentationPreloader.js index 00290a21c3..b43f1b10ed 100644 --- a/src/lib/viewers/doc/PresentationPreloader.js +++ b/src/lib/viewers/doc/PresentationPreloader.js @@ -3,18 +3,10 @@ import { CLASS_INVISIBLE, CLASS_BOX_PREVIEW_PRELOAD_WRAPPER_PRESENTATION } from import { setDimensions } from '../../util'; class PresentationPreloader extends DocPreloader { - /** - * @property {HTMLELement} - Maximum auto-zoom scale, set to 0 for no limit since presentation viewer doesn't - * have a maximum zoom scale and scales up to available viewport - */ + /** @inheritdoc */ maxZoomScale = 0; - /** - * [constructor] - * - * @param {PreviewUI} previewUI - UI instance - * @return {PresentationPreloader} PresentationPreloader instance - */ + /** @inheritdoc */ constructor(previewUI, options) { super(previewUI, options); @@ -35,11 +27,8 @@ class PresentationPreloader extends DocPreloader { return; } - setDimensions(this.imageEl, scaledWidth, scaledHeight); - setDimensions(this.overlayEl, scaledWidth, scaledHeight); - - // Hide the preview-level loading indicator - this.previewUI.hideLoadingIndicator(); + // Set initial placeholder dimensions + setDimensions(this.placeholderEl, scaledWidth, scaledHeight); // Show preload element after content is properly sized this.preloadEl.classList.remove(CLASS_INVISIBLE); diff --git a/src/lib/viewers/doc/__tests__/DocPreloader-test.js b/src/lib/viewers/doc/__tests__/DocPreloader-test.js index d8398000f0..42077c1f3d 100644 --- a/src/lib/viewers/doc/__tests__/DocPreloader-test.js +++ b/src/lib/viewers/doc/__tests__/DocPreloader-test.js @@ -1,11 +1,13 @@ /* eslint-disable no-unused-expressions */ +import * as util from '../../../util'; import Api from '../../../api'; import DocPreloader from '../DocPreloader'; -import * as util from '../../../util'; import { CLASS_BOX_PREVIEW_PRELOAD, + CLASS_BOX_PREVIEW_PRELOAD_BACKGROUND, CLASS_BOX_PREVIEW_PRELOAD_CONTENT, CLASS_BOX_PREVIEW_PRELOAD_OVERLAY, + CLASS_BOX_PREVIEW_PRELOAD_PLACEHOLDER, CLASS_INVISIBLE, CLASS_PREVIEW_LOADED, } from '../../../constants'; @@ -22,10 +24,9 @@ describe('lib/viewers/doc/DocPreloader', () => { containerEl = document.querySelector('.container'); stubs = {}; stubs.api = new Api(); - docPreloader = new DocPreloader({ hideLoadingIndicator: () => {} }, { api: stubs.api }); + docPreloader = new DocPreloader({}, { api: stubs.api }); docPreloader.previewUI = { - hideLoadingIndicator: jest.fn(), previewContainer: document.createElement('div'), }; }); @@ -56,6 +57,7 @@ describe('lib/viewers/doc/DocPreloader', () => { expect(docPreloader.wrapperEl).toContainSelector(`.${CLASS_BOX_PREVIEW_PRELOAD}`); expect(docPreloader.preloadEl).toContainSelector(`.${CLASS_BOX_PREVIEW_PRELOAD_CONTENT}`); expect(docPreloader.preloadEl).toContainSelector(`.${CLASS_BOX_PREVIEW_PRELOAD_OVERLAY}`); + expect(docPreloader.preloadEl).toContainSelector(`.${CLASS_BOX_PREVIEW_PRELOAD_PLACEHOLDER}`); expect(docPreloader.imageEl.src).toBe(imgSrc); expect(containerEl).toContainElement(docPreloader.wrapperEl); expect(docPreloader.bindDOMListeners).toBeCalled(); @@ -68,7 +70,6 @@ describe('lib/viewers/doc/DocPreloader', () => { stubs.checkDocumentLoaded = jest.spyOn(docPreloader, 'checkDocumentLoaded').mockImplementation(); stubs.emit = jest.spyOn(docPreloader, 'emit').mockImplementation(); stubs.setDimensions = jest.spyOn(util, 'setDimensions').mockImplementation(); - stubs.hideLoadingIndicator = docPreloader.previewUI.hideLoadingIndicator; docPreloader.imageEl = {}; docPreloader.preloadEl = document.createElement('div'); }); @@ -79,10 +80,9 @@ describe('lib/viewers/doc/DocPreloader', () => { docPreloader.scaleAndShowPreload(1, 1, 1); expect(stubs.setDimensions).not.toBeCalled(); - expect(stubs.hideLoadingIndicator).not.toBeCalled(); }); - test('should set preload image dimensions, hide loading indicator, show preload element, and emit preload event', () => { + test('should set preload image dimensions, show preload element, and emit preload event', () => { docPreloader.preloadEl.classList.add(CLASS_INVISIBLE); const width = 100; @@ -90,9 +90,7 @@ describe('lib/viewers/doc/DocPreloader', () => { docPreloader.scaleAndShowPreload(width, height, 1); - expect(stubs.setDimensions).toBeCalledWith(docPreloader.imageEl, width, height); - expect(stubs.setDimensions).toBeCalledWith(docPreloader.overlayEl, width, height); - expect(stubs.hideLoadingIndicator).toBeCalled(); + expect(stubs.setDimensions).toBeCalledWith(docPreloader.placeholderEl, width, height); expect(stubs.emit).toBeCalledWith('preload'); expect(docPreloader.preloadEl).not.toHaveClass(CLASS_INVISIBLE); }); @@ -102,7 +100,7 @@ describe('lib/viewers/doc/DocPreloader', () => { docPreloader.scaleAndShowPreload(100, 100, numPages); // Should scale 1 preload image, one overlay, and numPages - 1 placeholders - expect(stubs.setDimensions).toBeCalledTimes(numPages + 1); + expect(stubs.setDimensions).toBeCalledTimes(numPages); expect(docPreloader.preloadEl.children.length).toBe(numPages - 1); }); @@ -567,7 +565,13 @@ describe('lib/viewers/doc/DocPreloader', () => { }); jest.spyOn(util, 'setDimensions').mockImplementation(); docPreloader.preloadEl = document.createElement('div'); - docPreloader.preloadEl.innerHTML = `
`; + docPreloader.preloadEl.innerHTML = ` +
+
+ +
+
+ `.trim(); }); test('should short circuit if there is no preload element to resize', () => { @@ -613,7 +617,7 @@ describe('lib/viewers/doc/DocPreloader', () => { }; docPreloader.resize(); expect(docPreloader.getScaledWidthAndHeight).toBeCalled(); - expect(util.setDimensions).toBeCalledTimes(2); + expect(util.setDimensions).toBeCalledTimes(1); }); }); diff --git a/src/lib/viewers/doc/__tests__/PresentationPreloader-test.js b/src/lib/viewers/doc/__tests__/PresentationPreloader-test.js index 0bfc1cc087..a37df1ec50 100644 --- a/src/lib/viewers/doc/__tests__/PresentationPreloader-test.js +++ b/src/lib/viewers/doc/__tests__/PresentationPreloader-test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ -import PresentationPreloader from '../PresentationPreloader'; import * as util from '../../../util'; +import PresentationPreloader from '../PresentationPreloader'; import { CLASS_INVISIBLE } from '../../../constants'; let preloader; @@ -9,15 +9,8 @@ let stubs; describe('lib/viewers/doc/PresentationPreloader', () => { beforeEach(() => { fixture.load('viewers/doc/__tests__/PresentationPreloader-test.html'); - preloader = new PresentationPreloader({ - hideLoadingIndicator: () => {}, - }); - preloader.previewUI = { - hideLoadingIndicator: jest.fn(), - }; - stubs = { - hideLoadingIndicator: preloader.previewUI.hideLoadingIndicator, - }; + preloader = new PresentationPreloader({}); + stubs = {}; }); afterEach(() => { @@ -29,7 +22,7 @@ describe('lib/viewers/doc/PresentationPreloader', () => { stubs.checkDocumentLoaded = jest.spyOn(preloader, 'checkDocumentLoaded').mockImplementation(); stubs.emit = jest.spyOn(preloader, 'emit').mockImplementation(); stubs.setDimensions = jest.spyOn(util, 'setDimensions').mockImplementation(); - preloader.imageEl = {}; + preloader.placeholderEl = document.createElement('div'); preloader.preloadEl = document.createElement('div'); }); @@ -39,10 +32,9 @@ describe('lib/viewers/doc/PresentationPreloader', () => { preloader.scaleAndShowPreload(1, 1, 1); expect(stubs.setDimensions).not.toBeCalled(); - expect(stubs.hideLoadingIndicator).not.toBeCalled(); }); - test('should set preload image dimensions, hide loading indicator, show preload element, and emit preload event', () => { + test('should set preload image dimensions, show preload element, and emit preload event', () => { preloader.preloadEl.classList.add(CLASS_INVISIBLE); const width = 100; @@ -50,9 +42,7 @@ describe('lib/viewers/doc/PresentationPreloader', () => { preloader.scaleAndShowPreload(width, height, 1); - expect(stubs.setDimensions).toBeCalledWith(preloader.imageEl, width, height); - expect(stubs.setDimensions).toBeCalledWith(preloader.overlayEl, width, height); - expect(stubs.hideLoadingIndicator).toBeCalled(); + expect(stubs.setDimensions).toBeCalledWith(preloader.placeholderEl, width, height); expect(stubs.emit).toBeCalledWith('preload'); expect(preloader.preloadEl).not.toHaveClass(CLASS_INVISIBLE); }); diff --git a/src/lib/viewers/doc/_docBase.scss b/src/lib/viewers/doc/_docBase.scss index 215c7c89d0..3edd345151 100644 --- a/src/lib/viewers/doc/_docBase.scss +++ b/src/lib/viewers/doc/_docBase.scss @@ -1,8 +1,10 @@ @import '../../boxuiVariables'; @import './annotations'; +@import './loading'; $pdfjs-highlight: #b400aa !default; $pdfjs-highlight-selected: #006400 !default; +$pdfjs-page-padding: 15px; $thumbnail-border-radius: 3px; // Accounts for the 1px border on the right so the remaining width is actually 225 $thumbnail-sidebar-width: 226px; @@ -212,11 +214,11 @@ $thumbnail-sidebar-width: 226px; overflow: auto; } + //---------- Override CSS from generic PDFJS viewer build ----------// .pdfViewer.pinching { visibility: hidden; } - //---------- Override CSS from generic PDFJS viewer build ----------// .pdfViewer .page { position: relative; display: block; @@ -226,15 +228,17 @@ $thumbnail-sidebar-width: 226px; margin: 0 auto; // We use padding instead of margin to push down since we want to // include this padding when PDF.js jumps to pages - padding: 15px 0; + padding: $pdfjs-page-padding 0; overflow: visible; border: 0; border-image: none; - // Override loading icon styles from pdf.js - we use a CSS spinner absolutely centered + // Override loading icon styles from pdf.js - we use a ghost state .loadingIcon { - margin: auto; - background: none; + @include bp-Ghost; + + top: $pdfjs-page-padding; + bottom: $pdfjs-page-padding; } // Fixes annotation icon broken src @@ -253,7 +257,7 @@ $thumbnail-sidebar-width: 226px; } .textLayer { - top: 15px; // Match 15px padding top on page + top: $pdfjs-page-padding; bottom: auto; opacity: 1; diff --git a/src/lib/viewers/doc/_loading.scss b/src/lib/viewers/doc/_loading.scss new file mode 100644 index 0000000000..43727b78c5 --- /dev/null +++ b/src/lib/viewers/doc/_loading.scss @@ -0,0 +1,35 @@ +@import '~box-ui-elements/es/styles/constants/colors'; + +@keyframes bp-Ghost-keyframes { + 0% { + transform: translateX(-100%); + } + + 100% { + transform: translateX(200%); + } +} + +@mixin bp-Ghost() { + overflow: hidden; + background-color: $bdl-gray-10; + + &::before { + display: block; + width: 75%; + height: 100%; + background-image: linear-gradient(to right, $bdl-gray-10, lighten($bdl-gray-10, 2%) 50%, $bdl-gray-10 100%); + background-repeat: no-repeat; + transform: translateX(-50%); + animation: bp-Ghost-keyframes 1s linear infinite; + content: ''; + } + + .bp-theme-dark & { + background-color: $bdl-gray-62; + + &::before { + background-image: linear-gradient(to right, $bdl-gray-62, lighten($bdl-gray-62, 2%) 50%, $bdl-gray-62 100%); + } + } +}