UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

564 lines (560 loc) • 24.4 kB
/** * DevExtreme (ui/grid_core/ui.grid_core.virtual_scrolling_core.js) * Version: 18.1.3 * Build date: Tue May 15 2018 * * Copyright (c) 2012 - 2018 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ "use strict"; var $ = require("../../core/renderer"), window = require("../../core/utils/window").getWindow(), eventsEngine = require("../../events/core/events_engine"), browser = require("../../core/utils/browser"), positionUtils = require("../../animation/position"), each = require("../../core/utils/iterator").each, Class = require("../../core/class"), Deferred = require("../../core/utils/deferred").Deferred; var SCROLLING_MODE_INFINITE = "infinite", SCROLLING_MODE_VIRTUAL = "virtual"; var isVirtualMode = function(that) { return that.option("scrolling.mode") === SCROLLING_MODE_VIRTUAL || that._isVirtual }; var isAppendMode = function(that) { return that.option("scrolling.mode") === SCROLLING_MODE_INFINITE }; exports.getContentHeightLimit = function(browser) { if (browser.msie) { return 4e6 } else { if (browser.mozilla) { return 8e6 } } return 15e6 }; exports.subscribeToExternalScrollers = function($element, scrollChangedHandler, $targetElement) { var $scrollElement, scrollableArray = [], scrollToArray = [], disposeArray = []; $targetElement = $targetElement || $element; function getElementOffset(scrollable) { var $scrollableElement = scrollable.element ? scrollable.$element() : scrollable, scrollableOffset = positionUtils.offset($scrollableElement); if (!scrollableOffset) { return $element.offset().top } return scrollable.scrollTop() - (scrollableOffset.top - $element.offset().top) } function createWindowScrollHandler(scrollable) { return function() { var scrollTop = scrollable.scrollTop() - getElementOffset(scrollable); scrollTop = scrollTop > 0 ? scrollTop : 0; scrollChangedHandler(scrollTop) } } var widgetScrollStrategy = { on: function(scrollable, eventName, handler) { scrollable.on("scroll", handler) }, off: function(scrollable, eventName, handler) { scrollable.off("scroll", handler) } }; function subscribeToScrollEvents($scrollElement) { var isDocument = "#document" === $scrollElement.get(0).nodeName; var scrollable = $scrollElement.data("dxScrollable"); var eventsStrategy = widgetScrollStrategy; if (!scrollable) { scrollable = isDocument && $(window) || "auto" === $scrollElement.css("overflowY") && $scrollElement; eventsStrategy = eventsEngine; if (!scrollable) { return } } var handler = createWindowScrollHandler(scrollable); eventsStrategy.on(scrollable, "scroll", handler); scrollToArray.push(function(pos) { var topOffset = getElementOffset(scrollable), scrollMethod = scrollable.scrollTo ? "scrollTo" : "scrollTop"; if (pos - topOffset >= 0) { scrollable[scrollMethod](pos + topOffset) } }); scrollableArray.push(scrollable); disposeArray.push(function() { eventsStrategy.off(scrollable, "scroll", handler) }) } for ($scrollElement = $targetElement.parent(); $scrollElement.length; $scrollElement = $scrollElement.parent()) { subscribeToScrollEvents($scrollElement) } return { scrollTo: function(pos) { each(scrollToArray, function(_, scrollTo) { scrollTo(pos) }) }, dispose: function() { each(disposeArray, function(_, dispose) { dispose() }) } } }; exports.VirtualScrollController = Class.inherit(function() { var getViewportPageCount = function(that) { var pageSize = that._dataSource.pageSize(), preventPreload = that.option("scrolling.preventPreload"); if (preventPreload) { return 0 } var realViewportSize = that._viewportSize; if (isVirtualMode(that) && false === that.option("legacyRendering") && that.option("scrolling.removeInvisiblePages")) { realViewportSize = 0; var viewportSize = that._viewportSize * that._viewportItemSize; var offset = that.getContentOffset(); var position = that._position || 0; var virtualItemsCount = that.virtualItemsCount(); var totalItemsCount = that._dataSource.totalItemsCount(); var defaultItemSize = that.getItemSize(); for (var itemIndex = virtualItemsCount.begin; itemIndex < totalItemsCount; itemIndex++) { if (offset >= position + viewportSize) { break } var itemSize = that._itemSizes[itemIndex] || defaultItemSize; offset += itemSize; if (offset >= position) { realViewportSize++ } } } return pageSize && realViewportSize > 0 ? Math.ceil(realViewportSize / pageSize) : 1 }; var getPreloadPageCount = function(that, previous) { var preloadEnabled = that.option("scrolling.preloadEnabled"), pageCount = getViewportPageCount(that); if (pageCount) { if (previous) { pageCount = preloadEnabled ? 1 : 0 } else { if (preloadEnabled) { pageCount++ } if (isAppendMode(that)) { pageCount-- } } } return pageCount }; var currentPageIsLoaded = function(that) { var currentPageIndex = that._dataSource.pageIndex(); return that._cache.some(function(cacheItem) { return cacheItem.pageIndex === currentPageIndex }) }; var getPageIndexForLoad = function(that) { var needToLoadNextPage, needToLoadPrevPage, needToLoadPageBeforeLast, result = -1, beginPageIndex = getBeginPageIndex(that), dataSource = that._dataSource; if (beginPageIndex < 0) { result = that._pageIndex } else { if (!that._cache[that._pageIndex - beginPageIndex]) { if (currentPageIsLoaded(that) || that._isVirtual) { result = that._pageIndex } } else { if (beginPageIndex >= 0 && that._viewportSize >= 0) { if (beginPageIndex > 0) { needToLoadPageBeforeLast = getEndPageIndex(that) + 1 === dataSource.pageCount() && that._cache.length < getPreloadPageCount(that) + 1; needToLoadPrevPage = needToLoadPageBeforeLast || that._pageIndex === beginPageIndex && getPreloadPageCount(that, true); if (needToLoadPrevPage) { result = beginPageIndex - 1 } } if (result < 0) { needToLoadNextPage = beginPageIndex + that._cache.length <= that._pageIndex + getPreloadPageCount(that); if (needToLoadNextPage) { result = beginPageIndex + that._cache.length } } } } } return result }; var getBeginPageIndex = function(that) { return that._cache.length ? that._cache[0].pageIndex : -1 }; var getEndPageIndex = function(that) { return that._cache.length ? that._cache[that._cache.length - 1].pageIndex : -1 }; var fireChanged = function(that, changed, args) { that._isChangedFiring = true; changed(args); that._isChangedFiring = false }; var processDelayChanged = function(that, changed, args) { if (that._isDelayChanged) { that._isDelayChanged = false; fireChanged(that, changed, args); return true } }; var processChanged = function(that, changed, changeType, isDelayChanged, removeCacheItem) { var change, dataSource = that._dataSource, items = dataSource.items().slice(); if (changeType && !that._isDelayChanged) { change = { changeType: changeType, items: items }; if (removeCacheItem) { change.removeCount = removeCacheItem.itemsCount } } var viewportItems = that._dataSource.viewportItems(); if ("append" === changeType) { viewportItems.push.apply(viewportItems, items); if (removeCacheItem) { viewportItems.splice(0, removeCacheItem.itemsLength) } } else { if ("prepend" === changeType) { viewportItems.unshift.apply(viewportItems, items); if (removeCacheItem) { viewportItems.splice(-removeCacheItem.itemsLength) } } else { that._dataSource.viewportItems(items) } } dataSource.updateLoading(); that._lastPageIndex = that.pageIndex(); that._isDelayChanged = isDelayChanged; if (!isDelayChanged) { fireChanged(that, changed, change) } }; var loadCore = function(that, pageIndex) { var dataSource = that._dataSource; if (pageIndex === that.pageIndex() || !dataSource.isLoading() && pageIndex < dataSource.pageCount() || !dataSource.hasKnownLastPage() && pageIndex === dataSource.pageCount()) { dataSource.pageIndex(pageIndex); return dataSource.load() } }; return { ctor: function(component, dataSource, isVirtual) { var that = this; that._dataSource = dataSource; that.component = component; that._pageIndex = that._lastPageIndex = dataSource.pageIndex(); that._viewportSize = 0; that._viewportItemSize = 20; that._viewportItemIndex = -1; that._itemSizes = {}; that._sizeRatio = 1; that._items = []; that._cache = []; that._isVirtual = isVirtual }, option: function() { return this.component.option.apply(this.component, arguments) }, virtualItemsCount: function() { var pageIndex, beginItemsCount, endItemsCount, that = this, itemsCount = 0; if (isVirtualMode(that)) { pageIndex = getBeginPageIndex(that); if (pageIndex < 0) { pageIndex = that._dataSource.pageIndex() } beginItemsCount = pageIndex * that._dataSource.pageSize(); itemsCount = that._cache.length * that._dataSource.pageSize(); endItemsCount = Math.max(0, that._dataSource.totalItemsCount() - itemsCount - beginItemsCount); return { begin: beginItemsCount, end: endItemsCount } } }, _setViewportPositionCore: function(position, isNear) { var that = this, result = new Deferred, scrollingTimeout = Math.min(that.option("scrolling.timeout") || 0, that._dataSource.changingDuration()); if (scrollingTimeout < that.option("scrolling.renderingThreshold")) { scrollingTimeout = that.option("scrolling.minTimeout") || 0 } if (!isNear) { that._itemSizes = {} } clearTimeout(that._scrollTimeoutID); if (scrollingTimeout > 0) { that._scrollTimeoutID = setTimeout(function() { that.setViewportItemIndex(position); result.resolve() }, scrollingTimeout) } else { that.setViewportItemIndex(position); result.resolve() } return result.promise() }, getViewportPosition: function() { return this._position || 0 }, setViewportPosition: function(position) { var that = this, virtualItemsCount = that.virtualItemsCount(), defaultItemSize = that.getItemSize(), offset = that.getContentOffset(); that._position = position; if (virtualItemsCount && position >= offset && position <= offset + that._contentSize) { var itemSize, itemOffset = 0; do { itemSize = that._itemSizes[virtualItemsCount.begin + itemOffset] || defaultItemSize; offset += itemSize; itemOffset += offset < position ? 1 : (position - offset + itemSize) / itemSize } while (offset < position); return that._setViewportPositionCore(virtualItemsCount.begin + itemOffset, true) } else { return that._setViewportPositionCore(position / defaultItemSize) } }, setContentSize: function(size) { var that = this, sizes = Array.isArray(size) && size, virtualItemsCount = that.virtualItemsCount(); if (sizes) { size = sizes.reduce(function(a, b) { return a + b }, 0) } that._contentSize = size; if (virtualItemsCount) { if (sizes) { sizes.forEach(function(size, index) { that._itemSizes[virtualItemsCount.begin + index] = size }) } var virtualContentSize = (virtualItemsCount.begin + virtualItemsCount.end + that.itemsCount()) * that._viewportItemSize; var contentHeightLimit = exports.getContentHeightLimit(browser); if (virtualContentSize > contentHeightLimit) { that._sizeRatio = contentHeightLimit / virtualContentSize } else { that._sizeRatio = 1 } } }, getItemSize: function() { return this._viewportItemSize * this._sizeRatio }, getContentOffset: function(type) { var itemCount, that = this, virtualItemsCount = that.virtualItemsCount(), isEnd = "end" === type; if (!virtualItemsCount) { return 0 } itemCount = isEnd ? virtualItemsCount.end : virtualItemsCount.begin; var offset = 0, totalItemsCount = that._dataSource.totalItemsCount(); Object.keys(that._itemSizes).forEach(function(itemIndex) { if (isEnd ? itemIndex >= totalItemsCount - virtualItemsCount.end : itemIndex < virtualItemsCount.begin) { offset += that._itemSizes[itemIndex]; itemCount-- } }); return Math.floor(offset + itemCount * that._viewportItemSize * that._sizeRatio) }, getVirtualContentSize: function() { var that = this, virtualItemsCount = that.virtualItemsCount(); return virtualItemsCount ? (virtualItemsCount.begin + virtualItemsCount.end) * that._viewportItemSize * that._sizeRatio + that._contentSize : 0 }, getViewportItemIndex: function() { return this._viewportItemIndex }, setViewportItemIndex: function(itemIndex) { var lastPageSize, maxPageIndex, newPageIndex, that = this, pageSize = that._dataSource.pageSize(), pageCount = that._dataSource.pageCount(), virtualMode = isVirtualMode(that), appendMode = isAppendMode(that), totalItemsCount = that._dataSource.totalItemsCount(), needLoad = that._viewportItemIndex < 0; that._viewportItemIndex = itemIndex; if (pageSize && (virtualMode || appendMode) && totalItemsCount >= 0) { if (that._viewportSize && itemIndex + that._viewportSize >= totalItemsCount) { if (that._dataSource.hasKnownLastPage()) { newPageIndex = pageCount - 1; lastPageSize = totalItemsCount % pageSize; if (newPageIndex > 0 && lastPageSize > 0 && lastPageSize < pageSize / 2) { newPageIndex-- } } else { newPageIndex = pageCount } } else { newPageIndex = Math.floor(itemIndex / pageSize); maxPageIndex = pageCount - 1; newPageIndex = Math.max(newPageIndex, 0); newPageIndex = Math.min(newPageIndex, maxPageIndex) } if (that.pageIndex() !== newPageIndex || needLoad) { that.pageIndex(newPageIndex) } that.load() } }, viewportItemSize: function(size) { if (void 0 !== size) { this._viewportItemSize = size } return this._viewportItemSize }, viewportSize: function(size) { if (void 0 !== size) { this._viewportSize = size } return this._viewportSize }, pageIndex: function(_pageIndex) { if (isVirtualMode(this) || isAppendMode(this)) { if (void 0 !== _pageIndex) { this._pageIndex = _pageIndex } return this._pageIndex } else { return this._dataSource.pageIndex(_pageIndex) } }, beginPageIndex: function beginPageIndex(defaultPageIndex) { var beginPageIndex = getBeginPageIndex(this); if (beginPageIndex < 0) { beginPageIndex = void 0 !== defaultPageIndex ? defaultPageIndex : this.pageIndex() } return beginPageIndex }, endPageIndex: function endPageIndex() { var endPageIndex = getEndPageIndex(this); return endPageIndex > 0 ? endPageIndex : this._lastPageIndex }, load: function() { var pageIndexForLoad, result, dataSource = this._dataSource; if (isVirtualMode(this) || isAppendMode(this)) { pageIndexForLoad = getPageIndexForLoad(this); if (pageIndexForLoad >= 0) { result = loadCore(this, pageIndexForLoad) } dataSource.updateLoading() } else { result = dataSource.load() } if (!result && this._lastPageIndex !== this.pageIndex()) { this._dataSource.onChanged({ changeType: "pageIndex" }) } return result || (new Deferred).resolve() }, loadIfNeed: function() { var that = this; if ((isVirtualMode(that) || isAppendMode(that)) && !that._dataSource.isLoading() && !that._isChangedFiring) { that.load() } }, handleDataChanged: function(callBase) { var beginPageIndex, changeType, removeInvisiblePages, cacheItem, that = this, dataSource = that._dataSource, lastCacheLength = that._cache.length; if (isVirtualMode(that) || isAppendMode(that)) { beginPageIndex = getBeginPageIndex(that); if (beginPageIndex >= 0) { if (isVirtualMode(that) && beginPageIndex + that._cache.length !== dataSource.pageIndex() && beginPageIndex - 1 !== dataSource.pageIndex()) { that._cache = [] } if (isAppendMode(that)) { if (0 === dataSource.pageIndex()) { that._cache = [] } else { if (dataSource.pageIndex() < getEndPageIndex(that)) { fireChanged(that, callBase, { changeType: "append", items: [] }); return } } } } cacheItem = { pageIndex: dataSource.pageIndex(), itemsLength: dataSource.items().length, itemsCount: that.itemsCount(true) }; if (!that.option("legacyRendering") && that.option("scrolling.removeInvisiblePages") && isVirtualMode(that)) { removeInvisiblePages = that._cache.length > Math.max(getPreloadPageCount(this) + (that.option("scrolling.preloadEnabled") ? 1 : 0), 2) } else { processDelayChanged(that, callBase, { isDelayed: true }) } var removeCacheItem; if (beginPageIndex === dataSource.pageIndex() + 1) { if (removeInvisiblePages) { removeCacheItem = that._cache.pop() } changeType = "prepend"; that._cache.unshift(cacheItem) } else { if (removeInvisiblePages) { removeCacheItem = that._cache.shift() } changeType = "append"; that._cache.push(cacheItem) } processChanged(that, callBase, that._cache.length > 1 ? changeType : void 0, 0 === lastCacheLength, removeCacheItem); that._delayDeferred = that.load().done(function() { if (processDelayChanged(that, callBase)) { that.load() } }) } else { processChanged(that, callBase) } }, itemsCount: function itemsCount(isBase) { var itemsCount = 0; if (!isBase && isVirtualMode(this)) { each(this._cache, function() { itemsCount += this.itemsCount }) } else { itemsCount = this._dataSource.itemsCount() } return itemsCount }, reset: function() { this._cache = [] }, subscribeToWindowScrollEvents: function($element) { var that = this; that._windowScroll = that._windowScroll || exports.subscribeToExternalScrollers($element, function(scrollTop) { if (that.viewportItemSize()) { that.setViewportPosition(scrollTop) } }) }, dispose: function() { clearTimeout(this._scrollTimeoutID); this._windowScroll && this._windowScroll.dispose(); this._windowScroll = null }, scrollTo: function(pos) { this._windowScroll && this._windowScroll.scrollTo(pos) } } }());