viewer
Version:
A viewer for documents converted with the Box View API
526 lines (477 loc) • 16.3 kB
JavaScript
/**
* @fileoverview lazy-loader component definition
* @author lakenen
*/
/*global setTimeout, clearTimeout*/
/**
* lazy-loader component for controlling when pages should be loaded and unloaded
*/
Crocodoc.addComponent('lazy-loader', function (scope) {
'use strict';
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
var util = scope.getUtility('common'),
browser = scope.getUtility('browser'),
api = {},
pages,
numPages,
pagefocusTriggerLoadingTID,
readyTriggerLoadingTID,
pageLoadTID,
pageLoadQueue = [],
pageLoadRange = 1,
pageLoadingStopped = true,
scrollDirection = 1,
ready = false,
layoutState = {
page: 1,
visiblePages: [1]
};
/**
* Create and return a range object (eg., { min: x, max: y })
* for the current pageLoadRange constrained to the number of pages
* @param {int} range The range from current page
* @returns {Object} The range object
* @private
*/
function calculateRange(range) {
range = range || pageLoadRange;
var currentIndex = layoutState.page - 1,
low = currentIndex - range,
high = currentIndex + range;
return util.constrainRange(low, high, numPages - 1);
}
/**
* Loop through the pageLoadQueue and load pages sequentially,
* setting a timeout to run again after PAGE_LOAD_INTERVAL ms
* until the queue is empty
* @returns {void}
* @private
*/
function pageLoadLoop() {
var index;
clearTimeout(pageLoadTID);
if (pageLoadQueue.length > 0) {
// found a page to load
index = pageLoadQueue.shift();
// page exists and not reached max errors?
if (pages[index]) {
api.loadPage(index, function loadPageCallback(pageIsLoading) {
if (pageIsLoading === false) {
// don't wait if the page is not loading
pageLoadLoop();
} else {
pageLoadTID = setTimeout(pageLoadLoop, PAGE_LOAD_INTERVAL);
}
});
} else {
pageLoadLoop();
}
} else {
stopPageLoadLoop();
}
}
/**
* Start the page load loop
* @returns {void}
* @private
*/
function startPageLoadLoop() {
clearTimeout(pageLoadTID);
pageLoadingStopped = false;
pageLoadTID = setTimeout(pageLoadLoop, PAGE_LOAD_INTERVAL);
}
/**
* Stop the page load loop
* @returns {void}
* @private
*/
function stopPageLoadLoop() {
clearTimeout(pageLoadTID);
pageLoadingStopped = true;
}
/**
* Add a page to the page load queue and start the page
* load loop if necessary
* @param {int} index The index of the page to add
* @returns {void}
* @private
*/
function pushPageLoadQueue(index) {
pageLoadQueue.push(index);
if (pageLoadingStopped) {
startPageLoadLoop();
}
}
/**
* Clear all pages from the page load queue and stop the loop
* @returns {void}
* @private
*/
function clearPageLoadQueue() {
pageLoadQueue.length = 0;
stopPageLoadLoop();
}
/**
* Returns true if the given index is in the page load range, and false otherwise
* @param {int} index The page index
* @param {int} rangeLength The page range length
* @returns {bool} Whether the page index is in the page load range
* @private
*/
function indexInRange(index, rangeLength) {
var range = calculateRange(rangeLength);
if (index >= range.min && index <= range.max) {
return true;
}
return false;
}
/**
* Returns true if the given page index should be loaded, and false otherwise
* @param {int} index The page index
* @returns {bool} Whether the page should be loaded
* @private
*/
function shouldLoadPage(index) {
var page = pages[index];
// does the page exist?
if (page) {
// within page load range?
if (indexInRange(index)) {
return true;
}
// is it visible?
if (pageIsVisible(index)) {
return true;
}
}
return false;
}
/**
* Returns true if the given page index should be unloaded, and false otherwise
* @param {int} index The page index
* @param {int} rangeLength The range length
* @returns {bool} Whether the page should be unloaded
* @private
*/
function shouldUnloadPage(index, rangeLength) {
// within page load range?
if (indexInRange(index, rangeLength)) {
return false;
}
// is it visible?
if (pageIsVisible(index)) {
return false;
}
return true;
}
/**
* Returns true if the given page is visible, and false otherwise
* @param {int} index The page index
* @returns {bool} Whether the page is visible
* @private
*/
function pageIsVisible(index) {
// is it visible?
return util.inArray(index + 1, layoutState.visiblePages) > -1;
}
/**
* Queues pages to load in order from indexFrom to indexTo
* @param {number} start The page index to start at
* @param {number} end The page index to end at
* @returns {void}
*/
function queuePagesToLoadInOrder(start, end) {
var increment = util.sign(end - start);
while (start !== end) {
api.queuePageToLoad(start);
start += increment;
}
api.queuePageToLoad(start);
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return util.extend(api, {
messages: [
'beforezoom',
'pageavailable',
'pagefocus',
'ready',
'scroll',
'scrollend',
'zoom'
],
/**
* Handle framework messages
* @param {string} name The name of the message
* @param {Object} data The related data for the message
* @returns {void}
*/
onmessage: function (name, data) {
switch (name) {
case 'beforezoom':
this.handleBeforeZoom(data);
break;
case 'pageavailable':
this.handlePageAvailable(data);
break;
case 'pagefocus':
this.handlePageFocus(data);
break;
case 'ready':
this.handleReady();
break;
case 'scroll':
this.handleScroll();
break;
case 'scrollend':
this.handleScrollEnd();
break;
case 'zoom':
this.handleZoom(data);
break;
// no default
}
},
/**
* Initialize the LazyLoader component
* @param {Array} pageComponents The array of page components to lazily load
* @returns {void}
*/
init: function (pageComponents) {
pages = pageComponents;
numPages = pages.length;
pageLoadRange = (browser.mobile || browser.ielt10) ?
MAX_PAGE_LOAD_RANGE_MOBILE :
MAX_PAGE_LOAD_RANGE;
pageLoadRange = Math.min(pageLoadRange, numPages);
},
/**
* Destroy the LazyLoader component
* @returns {void}
*/
destroy: function () {
this.cancelAllLoading();
},
/**
* Updates the current layout state and scroll direction
* @param {Object} state The layout state
* @returns {void}
*/
updateLayoutState: function (state) {
scrollDirection = util.sign(state.page - layoutState.page);
layoutState = state;
},
/**
* Queue pages to load in the following order:
* 1) current page
* 2) visible pages
* 3) pages within pageLoadRange of the viewport
* @returns {void}
* @NOTE: this function is debounced so it will not load and abort
* several times if called a lot in a short time
*/
loadNecessaryPages: util.debounce(100, function () {
// cancel anything that happens to be loading first
this.cancelAllLoading();
// load current page first
this.queuePageToLoad(layoutState.page - 1);
// then load pages that are visible in the viewport
this.loadVisiblePages();
// then load pages beyond the viewport
this.loadPagesInRange(pageLoadRange);
}),
/**
* Queue pages to load within the given range such that
* proceeding pages are added before preceding pages
* @param {int} range The range to load beyond the current page
* @returns {void}
*/
loadPagesInRange: function (range) {
var currentIndex = layoutState.page - 1;
if (range > 0) {
range = calculateRange(range);
// load pages in the order of priority based on the direction
// the user is scrolling (load nearest page first, working in
// the scroll direction, then start on the opposite side of
// scroll direction and work outward)
// NOTE: we're assuming that a negative scroll direction means
// direction of previous pages, and positive is next pages...
if (scrollDirection >= 0) {
queuePagesToLoadInOrder(currentIndex + 1, range.max);
queuePagesToLoadInOrder(currentIndex - 1, range.min);
} else {
queuePagesToLoadInOrder(currentIndex - 1, range.min);
queuePagesToLoadInOrder(currentIndex + 1, range.max);
}
}
},
/**
* Queue to load all pages that are visible according
* to the current layoutState
* @returns {void}
*/
loadVisiblePages: function () {
var i, len;
for (i = 0, len = layoutState.visiblePages.length; i < len; ++i) {
this.queuePageToLoad(layoutState.visiblePages[i] - 1);
}
},
/**
* Add the page at the given index to the page load queue
* and call the preload function on the page
* @param {int} index The index of the page to load
* @returns {void}
*/
queuePageToLoad: function (index) {
if (shouldLoadPage(index)) {
pages[index].preload();
pushPageLoadQueue(index);
}
},
/**
* Clear the page load queue
* @returns {void}
*/
cancelAllLoading: function () {
clearTimeout(readyTriggerLoadingTID);
clearTimeout(pagefocusTriggerLoadingTID);
clearPageLoadQueue();
},
/**
* Call the load method on the page object at the specified index
* @param {int} index The index of the page to load
* @param {Function} callback Callback function to call always (regardless of page load success/fail)
* @returns {void}
*/
loadPage: function (index, callback) {
$.when(pages[index] && pages[index].load())
.always(callback);
},
/**
* Call the unload method on the page object at the specified index
* @param {int} index The page index
* @returns {void}
*/
unloadPage: function (index) {
var page = pages[index];
if (page) {
page.unload();
}
},
/**
* Unload all pages that are not within the given range (nor visible)
* @param {int} rangeLength The page range length
* @returns {void}
*/
unloadUnnecessaryPages: function (rangeLength) {
var i, l;
// remove out-of-range SVG from DOM
for (i = 0, l = pages.length; i < l; ++i) {
if (shouldUnloadPage(i, rangeLength)) {
this.unloadPage(i);
}
}
},
/**
* Handle ready messages
* @returns {void}
*/
handleReady: function () {
ready = true;
this.loadVisiblePages();
readyTriggerLoadingTID = setTimeout(function () {
api.loadNecessaryPages();
}, READY_TRIGGER_PRELOADING_DELAY);
},
/**
* Handle pageavailable messages
* @param {Object} data The message data
* @returns {void}
*/
handlePageAvailable: function (data) {
if (!ready) {
return;
}
var i;
if (data.all === true) {
data.upto = numPages;
}
if (data.page) {
this.queuePageToLoad(data.page - 1);
} else if (data.upto) {
for (i = 0; i < data.upto; ++i) {
this.queuePageToLoad(i);
}
}
},
/**
* Handle pagefocus messages
* @param {Object} data The message data
* @returns {void}
*/
handlePageFocus: function (data) {
// NOTE: update layout state before `ready`
this.updateLayoutState(data);
if (!ready) {
return;
}
this.cancelAllLoading();
// set a timeout to trigger loading so we dont cause unnecessary layouts while scrolling
pagefocusTriggerLoadingTID = setTimeout(function () {
api.loadNecessaryPages();
}, 200);
},
/**
* Handle beforezoom messages
* @param {Object} data The message data
* @returns {void}
*/
handleBeforeZoom: function (data) {
if (!ready) {
return;
}
this.cancelAllLoading();
// @NOTE: for performance reasons, we unload as many pages as possible just before zooming
// so we don't have to layout as many pages at a time immediately after the zoom.
// This is arbitrarily set to 2x the number of visible pages before the zoom, and
// it seems to work alright.
this.unloadUnnecessaryPages(data.visiblePages.length * 2);
},
/**
* Handle zoom messages
* @param {Object} data The message data
* @returns {void}
*/
handleZoom: function (data) {
// NOTE: update layout state before `ready`
this.updateLayoutState(data);
if (!ready) {
return;
}
this.loadNecessaryPages();
},
/**
* Handle scroll messages
* @param {Object} data The message data
* @returns {void}
*/
handleScroll: function () {
this.cancelAllLoading();
},
/**
* Handle scrollend messages
* @param {Object} data The message data
* @returns {void}
*/
handleScrollEnd: function () {
if (!ready) {
return;
}
this.loadNecessaryPages();
this.unloadUnnecessaryPages(pageLoadRange);
}
});
});