@revolist/revogrid
Version:
Virtual reactive data grid spreadsheet component - RevoGrid.
521 lines (514 loc) • 19.9 kB
JavaScript
/*!
* Built by Revolist OU ❤️
*/
import { proxyCustomElement, HTMLElement, createEvent, h, Host } from '@stencil/core/internal/client';
import { n as createStore, i as setStore, D as DataStore } from './data.store.js';
import { c as ROW_HEADER_TYPE } from './consts.js';
import './platform.js';
import { g as getItemByPosition } from './dimension.helpers.js';
import { H as HEADER_SLOT, d as defineCustomElement$2 } from './revogr-viewport-scroll2.js';
import { d as defineCustomElement$4 } from './revogr-data2.js';
import { b as defineCustomElement$3 } from './revogr-header2.js';
import { d as defineCustomElement$1 } from './vnode-converter.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 = 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 = 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;
}
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 || [] });
}
setStore(this.store, data);
}
}
const RowHeaderRender = s => (__, { rowIndex: i }) => s + i;
const RevogrRowHeaders = /*@__PURE__*/ proxyCustomElement(class RevogrRowHeaders extends HTMLElement {
constructor() {
super();
this.__registerHost();
this.scrollViewport = createEvent(this, "scrollview", 3);
this.elementToScroll = createEvent(this, "ref", 3);
this.height = undefined;
this.dataPorts = undefined;
this.headerProp = undefined;
this.rowClass = undefined;
this.resize = undefined;
this.rowHeaderColumn = undefined;
this.additionalData = undefined;
this.jobsBeforeRender = [];
}
render() {
const dataViews = [];
const viewport = new ViewportStore('colPinStart');
/** render viewports rows */
let totalLength = 1;
// todo: this part could be optimized to avoid to often re-render dataPorts can be cached
for (let data of this.dataPorts) {
const itemCount = data.dataStore.get('items').length;
// initiate row data
const dataStore = new DataStore(data.type, Object.assign({}, data.dataStore.state));
// initiate column data
const colData = new DataStore('colPinStart');
const column = Object.assign({ cellTemplate: RowHeaderRender(totalLength) }, this.rowHeaderColumn);
colData.updateData([column]);
dataViews.push(h("revogr-data", Object.assign({}, data, { colType: "rowHeaders", jobsBeforeRender: this.jobsBeforeRender, rowClass: this.rowClass, dataStore: dataStore.store, colData: colData.store, viewportCol: viewport.store, readonly: true, range: false })));
totalLength += itemCount;
}
const colSize = calculateRowHeaderSize(totalLength, this.rowHeaderColumn);
viewport.setViewport({
realCount: 1,
virtualSize: 0,
items: [
{
size: colSize,
start: 0,
end: colSize,
itemIndex: 0,
},
],
});
const viewportScroll = {
contentHeight: this.height,
contentWidth: 0,
style: { minWidth: `${colSize}px` },
colType: 'rowHeaders',
ref: (el) => this.elementToScroll.emit(el),
onScrollviewport: (e) => this.scrollViewport.emit(e.detail),
};
const viewportHeader = Object.assign(Object.assign({}, this.headerProp), {
// groups not present on row headers
groups: [], colData: typeof this.rowHeaderColumn === 'object' ? [this.rowHeaderColumn] : [], viewportCol: viewport.store, canResize: false, type: ROW_HEADER_TYPE,
// parent,
slot: HEADER_SLOT
});
return (h(Host, { class: { [ROW_HEADER_TYPE]: true }, key: ROW_HEADER_TYPE }, h("revogr-viewport-scroll", Object.assign({ key: 'c401e82e02e4bdb7afb25f2f49c6776f2e115c81' }, viewportScroll, { "row-header": true }), h("revogr-header", Object.assign({ key: '3c73d27bd96e23a34fc0cf47eda4d2e65751df98' }, viewportHeader)), dataViews)));
}
}, [0, "revogr-row-headers", {
"height": [2],
"dataPorts": [16],
"headerProp": [16],
"rowClass": [1, "row-class"],
"resize": [4],
"rowHeaderColumn": [16],
"additionalData": [8, "additional-data"],
"jobsBeforeRender": [16]
}]);
function defineCustomElement() {
if (typeof customElements === "undefined") {
return;
}
const components = ["revogr-row-headers", "revogr-data", "revogr-header", "revogr-viewport-scroll", "vnode-html"];
components.forEach(tagName => { switch (tagName) {
case "revogr-row-headers":
if (!customElements.get(tagName)) {
customElements.define(tagName, RevogrRowHeaders);
}
break;
case "revogr-data":
if (!customElements.get(tagName)) {
defineCustomElement$4();
}
break;
case "revogr-header":
if (!customElements.get(tagName)) {
defineCustomElement$3();
}
break;
case "revogr-viewport-scroll":
if (!customElements.get(tagName)) {
defineCustomElement$2();
}
break;
case "vnode-html":
if (!customElements.get(tagName)) {
defineCustomElement$1();
}
break;
} });
}
export { RevogrRowHeaders as R, ViewportStore as V, addMissingItems as a, getItems as b, isActiveRangeOutsideLastItem as c, getFirstItem as d, getLastItem as e, calculateRowHeaderSize as f, getUpdatedItemsByPosition as g, defineCustomElement as h, isActiveRange as i, recombineByOffset as r, setItemSizes as s, updateMissingAndRange as u };
//# sourceMappingURL=revogr-row-headers2.js.map