@revolist/revogrid
Version:
Virtual reactive data grid spreadsheet component - RevoGrid.
419 lines (413 loc) • 15.1 kB
JavaScript
/*!
* Built by Revolist OU ❤️
*/
'use strict';
const dimension_helpers = require('./dimension.helpers-29797371.js');
const LETTER_BLOCK_SIZE = 10;
const calculateRowHeaderSize = (itemsLength, rowHeaderColumn, minWidth = 50) => {
return ((rowHeaderColumn === null || rowHeaderColumn === void 0 ? void 0 : rowHeaderColumn.size) ||
Math.max((itemsLength.toString().length + 1) * LETTER_BLOCK_SIZE, minWidth));
};
/**
* Update items based on new scroll position
* If viewport wasn't changed fully simple recombination of positions
* Otherwise rebuild viewport items
*/
function getUpdatedItemsByPosition(pos, // coordinate
items, realCount, virtualSize, dimension) {
const activeItem = dimension_helpers.getItemByPosition(dimension, pos);
const firstItem = getFirstItem(items);
let toUpdate;
// do simple position recombination if items already present in viewport
if (firstItem) {
let changedOffsetStart = activeItem.itemIndex - (firstItem.itemIndex || 0);
// if item changed
if (changedOffsetStart) {
// simple recombination
toUpdate = recombineByOffset(Math.abs(changedOffsetStart), Object.assign(Object.assign({ positiveDirection: changedOffsetStart > -1 }, dimension), items));
}
}
const maxSizeVirtualSize = getMaxVirtualSize(virtualSize, dimension.realSize, activeItem);
// if partial recombination add items if revo-viewport has some space left
if (toUpdate) {
const extra = addMissingItems(activeItem, realCount, maxSizeVirtualSize, toUpdate, dimension);
if (extra.length) {
updateMissingAndRange(toUpdate.items, extra, toUpdate);
}
}
// new collection if no items after replacement full replacement
if (!toUpdate) {
const items = getItems({
firstItemStart: activeItem.start,
firstItemIndex: activeItem.itemIndex,
origSize: dimension.originItemSize,
maxSize: maxSizeVirtualSize,
maxCount: realCount,
sizes: dimension.sizes,
});
// range now comes from 0 to length - 1
toUpdate = {
items,
start: 0,
end: items.length - 1,
};
}
return toUpdate;
}
// virtual size can differ based on scroll position if some big items are present
// scroll can be in the middle of item and virtual size will be larger
// so we need to exclude this part from virtual size hence it's already passed
function getMaxVirtualSize(virtualSize, realSize, activeItem) {
return Math.min(virtualSize + (activeItem.end - activeItem.start), realSize);
}
function updateMissingAndRange(items, missing, range) {
items.splice(range.end + 1, 0, ...missing);
// update range if start larger after recombination
if (range.start >= range.end &&
!(range.start === range.end && range.start === 0)) {
range.start += missing.length;
}
range.end += missing.length;
}
/**
* If partial replacement
* this function adds items if viewport has some space left
*/
function addMissingItems(firstItem, realCount, virtualSize, existingCollection, dimension) {
const lastItem = getLastItem(existingCollection);
const items = getItems({
sizes: dimension.sizes,
firstItemStart: lastItem.end,
firstItemIndex: lastItem.itemIndex + 1,
origSize: dimension.originItemSize,
maxSize: virtualSize - (lastItem.end - firstItem.start),
maxCount: realCount,
});
return items;
}
/**
* Get wiewport items parameters
* caching position and calculating items count in viewport
*/
function getItems(opt, currentSize = 0) {
const items = [];
let index = opt.firstItemIndex;
let size = currentSize;
// max size or max count
while (size <= opt.maxSize && index < opt.maxCount) {
const newSize = getItemSize(index, opt.sizes, opt.origSize);
items.push({
start: opt.firstItemStart + size,
end: opt.firstItemStart + size + newSize,
itemIndex: index,
size: newSize,
});
size += newSize;
index++;
}
return items;
}
function recombineByOffset(offset, data) {
var _a, _b;
const newItems = [...data.items];
const itemsCount = newItems.length;
let newRange = {
start: data.start,
end: data.end,
};
// if offset out of revo-viewport, makes sense whole redraw
if (offset > itemsCount) {
return undefined;
}
// is direction of scroll positive
if (data.positiveDirection) {
// push item to the end
let lastItem = getLastItem(data);
let i = newRange.start;
const length = i + offset;
for (; i < length; i++) {
const newIndex = lastItem.itemIndex + 1;
const size = getItemSize(newIndex, data.sizes, data.originItemSize);
// if item overlapped limit break a loop
if (lastItem.end + size > data.realSize) {
break;
}
// new item index to recombine
let newEnd = i % itemsCount;
// item should always present, we do not create new item, we recombine them
if (!newItems[newEnd]) {
throw new Error('incorrect index');
}
// do recombination
newItems[newEnd] = lastItem = {
start: lastItem.end,
end: lastItem.end + size,
itemIndex: newIndex,
size: size,
};
// update range
newRange.start++;
newRange.end = newEnd;
}
// direction is negative
}
else {
// push item to the start
let firstItem = getFirstItem(data);
const end = newRange.end;
for (let i = 0; i < offset; i++) {
const newIndex = ((_a = firstItem === null || firstItem === void 0 ? void 0 : firstItem.itemIndex) !== null && _a !== void 0 ? _a : 0) - 1;
const size = getItemSize(newIndex, data.sizes, data.originItemSize);
// new item index to recombine
let newStart = end - i;
newStart = (newStart < 0 ? itemsCount + newStart : newStart) % itemsCount;
// item should always present, we do not create new item, we recombine them
if (!newItems[newStart]) {
console.error('incorrect index');
break;
}
// do recombination
const firstItemStart = (_b = firstItem === null || firstItem === void 0 ? void 0 : firstItem.start) !== null && _b !== void 0 ? _b : 0;
newItems[newStart] = firstItem = {
start: firstItemStart - size,
end: firstItemStart,
itemIndex: newIndex,
size: size,
};
// update range
newRange.start = newStart;
newRange.end--;
}
}
const range = {
start: (newRange.start < 0 ? itemsCount + newRange.start : newRange.start) %
itemsCount,
end: (newRange.end < 0 ? itemsCount + newRange.end : newRange.end) %
itemsCount,
};
return Object.assign({ items: newItems }, range);
}
function getItemSize(index, sizes, origSize = 0) {
if (sizes && sizes[index]) {
return sizes[index];
}
return origSize;
}
/**
* Verify if position is in range of the PositionItem, start and end are included
*/
function isActiveRange(pos, realSize, first, last) {
if (!first || !last) {
return false;
}
// if position is in range of first item
// or position is after first item and last item is the last item in real size
return ((pos >= first.start && pos <= first.end) ||
(pos > first.end && last.end === realSize));
}
function isActiveRangeOutsideLastItem(pos, virtualSize, firstItem, lastItem) {
var _a;
// if no first item, means no items in viewport
if (!firstItem) {
return false;
}
return virtualSize + pos > ((_a = lastItem === null || lastItem === void 0 ? void 0 : lastItem.end) !== null && _a !== void 0 ? _a : 0);
}
function getFirstItem(s) {
return s.items[s.start];
}
function getLastItem(s) {
return s.items[s.end];
}
/**
* Set items sizes from start index to end
* @param vpItems
* @param start
* @param size
* @param lastCoordinate
* @returns
*/
function setItemSizes(vpItems, initialIndex, size, lastCoordinate) {
const items = [...vpItems];
const count = items.length;
let pos = lastCoordinate;
let i = 0;
let start = initialIndex;
// viewport not inited
if (!count) {
return [];
}
// loop through array from initial item after recombination
while (i < count) {
const item = items[start];
item.start = pos;
item.size = size;
item.end = item.start + size;
pos = item.end;
// loop by start index
start++;
i++;
// if start index out of array, reset it
if (start === count) {
start = 0;
}
}
return items;
}
/**
* Viewport store
* Used for virtualization (process of rendering only visible part of data)
* Redraws viewport based on position and dimension
*/
function initialState() {
return {
// virtual item information per rendered item
items: [],
// virtual dom item order to render
start: 0,
end: 0,
// size of virtual viewport in px
virtualSize: 0,
// total number of items
realCount: 0,
// size of viewport in px
clientSize: 0,
};
}
/**
* Viewport store class
*/
class ViewportStore {
get lastCoordinate() {
return this.lastKnownScroll;
}
set lastCoordinate(value) {
this.lastKnownScroll = value;
}
constructor(type) {
this.type = type;
// last coordinate for store position restore
this.lastKnownScroll = 0;
this.store = dimension_helpers.createStore(initialState());
}
/**
* Render viewport based on coordinate
* It's the main method for draw
* Use force if you want to re-render viewport
*/
setViewPortCoordinate(position, dimension, force = false) {
const viewportSize = this.store.get('virtualSize');
// no visible data to calculate
if (!viewportSize) {
return;
}
const frameOffset = 1;
const singleOffsetInPx = dimension.originItemSize * frameOffset;
// add offset to virtual size from both sides
const outsize = singleOffsetInPx * 2;
// math virtual size is based on visible area + 2 items outside of visible area
const virtualSize = viewportSize + outsize;
// expected no scroll if real size less than virtual size, position is 0
let maxCoordinate = 0;
// if there is nodes outside of viewport, max coordinate has to be adjusted
if (dimension.realSize > viewportSize) {
// max coordinate is real size minus virtual/rendered space
maxCoordinate = dimension.realSize - viewportSize - singleOffsetInPx;
}
let pos = position;
// limit position to max and min coordinates
if (pos < 0) {
pos = 0;
}
else if (pos > maxCoordinate) {
pos = maxCoordinate;
}
// store last coordinate for further restore on redraw
this.lastCoordinate = pos;
// actual position is less than first item start based on offset
pos -= singleOffsetInPx;
pos = pos < 0 ? 0 : pos < maxCoordinate ? pos : maxCoordinate;
let allItems;
// if force clear all items and start from 0
if (force) {
allItems = {
items: [],
start: 0,
end: 0,
};
}
else {
allItems = this.getItems();
}
const firstItem = getFirstItem(allItems);
const lastItem = getLastItem(allItems);
let toUpdate = {};
// left position changed
// verify if new position is in range of previously rendered first item
if (!isActiveRange(pos, dimension.realSize, firstItem, lastItem)) {
toUpdate = Object.assign(Object.assign({}, toUpdate), getUpdatedItemsByPosition(pos, allItems, this.store.get('realCount'), virtualSize, dimension));
this.setViewport(Object.assign({}, toUpdate));
// verify is render area is outside of last item
}
else if (isActiveRangeOutsideLastItem(pos, virtualSize, firstItem, lastItem)) {
const items = [...allItems.items];
// check is any item missing for fulfill content
const missing = addMissingItems(firstItem, this.store.get('realCount'), virtualSize + pos - firstItem.start, allItems, {
sizes: dimension.sizes,
originItemSize: dimension.originItemSize,
});
// update missing items
if (missing.length) {
const range = {
start: this.store.get('start'),
end: this.store.get('end'),
};
updateMissingAndRange(items, missing, range);
toUpdate = Object.assign(Object.assign(Object.assign({}, toUpdate), { items: [...items] }), range);
this.setViewport(Object.assign({}, toUpdate));
}
}
}
/**
* Set sizes for existing items
*/
setOriginalSizes(size) {
const items = this.store.get('items');
const count = items.length;
// viewport not inited
if (!count) {
return;
}
dimension_helpers.setStore(this.store, {
items: setItemSizes(items, this.store.get('start'), size, this.lastCoordinate),
});
}
getItems() {
return {
items: this.store.get('items'),
start: this.store.get('start'),
end: this.store.get('end'),
};
}
setViewport(data) {
// drop items on virtual size change, require a new item set
// drop items on real size change, require a new item set
if (typeof data.realCount === 'number' || typeof data.virtualSize === 'number') {
data = Object.assign(Object.assign({}, data), { items: data.items || [] });
}
dimension_helpers.setStore(this.store, data);
}
}
exports.ViewportStore = ViewportStore;
exports.addMissingItems = addMissingItems;
exports.calculateRowHeaderSize = calculateRowHeaderSize;
exports.getFirstItem = getFirstItem;
exports.getItems = getItems;
exports.getLastItem = getLastItem;
exports.getUpdatedItemsByPosition = getUpdatedItemsByPosition;
exports.isActiveRange = isActiveRange;
exports.isActiveRangeOutsideLastItem = isActiveRangeOutsideLastItem;
exports.recombineByOffset = recombineByOffset;
exports.setItemSizes = setItemSizes;
exports.updateMissingAndRange = updateMissingAndRange;
//# sourceMappingURL=viewport.store-7f0d0f80.js.map