bbpro
Version:
BrowserBox - remote browser isolation tool
455 lines (411 loc) • 13.1 kB
JavaScript
class Infin extends Base {
// state
// hidden variables
#updating = false;
#yer;
#viewport;
#xdirection = 0;
#ydirection = 0;
#lastScrollTop = 0;
#lastScrollLeft = 0;
#lastYDirection = 0;
#lastXDirection = 0;
#yrejig = false;
#xrejig = false;
#cols = [];
#colPool = [];
get viewport() {
if ( this.#viewport ) return this.#viewport;
this.#viewport = this.shadowRoot.querySelector('.box');
return this.#viewport;
}
constructor() {
super();
// API sketches / ideas
// setWidth(x)
// setHeight(y)
// set the width and height CSS variable properties
// to set the size of the table element
// setRowCount(p)
// setColumnCount(q)
// setRowHeight(height, index = ALL)
// setColumnWidth(width, index = ALL)
// setRowLabels(labels = [1, 2, 3, ...])
// setColumnLabels(labels = ['A', 'B', ...., 'AA', 'AB', ...])
// setScrollLeft(x)
// setScrollTop(y)
// scrollToColumn(c: string Label | number index)
// scrollToRow(r: string Label | number index)
// setCellValue(x, y, {value, formula, ...etc})
// getCellValue
// getViewportDimensions() : {rows, columns, startRow, startColumn}
// setViewportData(<[cellLabel]: cellData> data) : true | throws
// if data not big enough or has incorrect labels
// setDataSource({labelTranslator, dataTransformer})
// sets logical data source for entire table
// automatically sets viewport with correct data after scroll
this.untilVisible().then(() => {
this.#yer = new IntersectionObserver(
entries => entries.forEach(entry => {
this.topToPool();
this.bottomToPool();
if ( this.#ydirection >= 0 ) {
this.poolToBottom();
} else {
this.poolToTop();
}
}),
{root: this.viewport}
);
// so when the first row crosses threshold, put new one on bottom
// when last row crosses threashold, put one on top
let start = 0;
Array.from(this.viewport.querySelectorAll('tr.sc-item')).forEach(row => {
row.style.top = `${start}px`;
this.#yer.observe(row);
start += row.clientHeight;
{
let cellStart = parseFloat(row.style.left || this.viewport.scrollLeft);
Array.from(row.querySelectorAll('td')).forEach((cell,j) => {
let col = this.#cols[j];
if ( ! col ) {
const processEntries = () => {
col.entries.length = 0;
this.handleEntry();
};
col = {
entries: [],
left: cellStart,
width: cell.clientWidth,
observer: new IntersectionObserver(
entries => {
entries.forEach(entry => col.entries.push(entry));
setTimeout(processEntries, 0);
},
{root: this.viewport}
),
cells: []
};
this.#cols[j] = col;
}
cell.style.left = `${cellStart}px`;
col.cells.push(cell);
col.observer.observe(cell);
cellStart += cell.clientWidth;
});
}
});
this.topToPool();
this.bottomToPool();
this.leftToPool();
this.rightToPool();
});
}
handleEntry() {
if ( this.#xdirection == 0 ) return;
this.leftToPool();
this.rightToPool();
if ( this.#xdirection >= 0 ) {
this.poolToRight();
} else {
this.poolToLeft();
}
}
Recalculate() {
// do nothing
}
rowsAbove() {
const first = [];
const start = this.viewport.scrollTop;
Array.from(this.viewport.querySelectorAll('tbody tr.sc-item'))
.find(el => {
const thisTop = parseFloat(el.style.top) + el.clientHeight;
if ( thisTop < start ) {
first.push(el);
}
});
return first;
}
rowsBelow() {
const last = [];
const end = this.viewport.scrollTop + this.viewport.clientHeight;
Array.from(this.viewport.querySelectorAll('tbody tr.sc-item'))
.find(el => {
const thisTop = parseFloat(el.style.top);
if ( thisTop > end ) {
last.push(el);
}
});
return last;
}
colsLeft() {
const left = [];
const start = this.viewport.scrollLeft;
this.#cols = this.#cols
.filter((col,i) => {
const thisLeft = col.left + col.width;
if ( thisLeft < start ) {
left.push(col);
return false;
}
return true;
});
return left;
}
colsRight() {
const right = [];
const end = this.viewport.scrollLeft + this.viewport.clientWidth;
this.#cols = this.#cols
.filter((col,i) => {
const thisLeft = col.left;
if ( thisLeft > end ) {
right.push(col);
return false;
}
return true;
});
return right;
}
allRows() {
return Array.from(this.viewport.querySelectorAll('tbody tr'));
}
leftmostCellLeft() {
let leftmostCellLeft = Infinity;
this.#cols.forEach(col => {
const left = col.left;
if ( left < leftmostCellLeft ) {
leftmostCellLeft = left;
}
});
return leftmostCellLeft;
}
rightmostCellRight() {
let rightmostCellRight = -Infinity;
this.#cols.forEach(col => {
const right = col.left + col.width;
if ( right > rightmostCellRight ) {
rightmostCellRight = right;
}
});
return rightmostCellRight;
}
highestRowTop() {
let highestRowTop = Infinity;
Array.from(this.viewport.querySelectorAll('tbody tr.sc-item')).forEach(el => {
const top = parseFloat(el.style.top);
if ( top < highestRowTop ) {
highestRowTop = top;
}
});
return highestRowTop;
}
lowestRowBottom() {
let lowestRowBottom = -Infinity;
Array.from(this.viewport.querySelectorAll('tbody tr.sc-item')).forEach(el => {
const bottom = parseFloat(el.style.top) + el.clientHeight;
if ( bottom > lowestRowBottom ) {
lowestRowBottom = bottom;
}
});
return lowestRowBottom;
}
allToPool() {
this.allRows().forEach(this.toPool);
}
topToPool() {
this.rowsAbove().forEach(this.toPool);
}
bottomToPool() {
this.rowsBelow().forEach(this.toPool);
}
leftToPool() {
this.colsLeft().forEach(col => this.toPoolCol(col));
}
rightToPool() {
this.colsRight().forEach(col => this.toPoolCol(col));
}
allCellsToPool() {
this.#cols.forEach(col => this.toPoolCol(col));
this.#cols.length = 0;
}
poolToLeft(atST = false, lockScroll = this.viewport.scrollLeft) {
//lockScroll = this.viewport.scrollLeft;
let BUFFER = Math.max(0, lockScroll - 150);
let i = 0, pool, leftmostCellLeft;
leftmostCellLeft = this.leftmostCellLeft();
if ( atST || leftmostCellLeft == Infinity ) {
if ( atST ) {
leftmostCellLeft = lockScroll + this.viewport.clientWidth;
} else {
leftmostCellLeft = BUFFER;
BUFFER -= this.viewport.clientWidth;
}
this.allCellsToPool();
}
do {
pool = this.#colPool.shift();
if ( pool ) {
this.#cols.unshift(pool);
i++;
pool.left = leftmostCellLeft-pool.width;
pool.cells.forEach(cell => {
cell.classList.remove('sc-cell');
cell.classList.add('sc-item');
cell.style.top = 0;
cell.style.left = `${pool.left}px`;
cell.style.width = `${pool.width}px`;
});
leftmostCellLeft -= pool.width;
}
} while( (i < 1 || atST) && pool && leftmostCellLeft > BUFFER );
}
poolToRight(r, atST = false, lockScroll = this.viewport.scrollLeft) {
//lockScroll = this.viewport.scrollLeft;
let BUFFER = lockScroll + this.viewport.clientWidth + 150;
let i = 0, pool, rightmostCellRight;
rightmostCellRight = this.rightmostCellRight();
if ( atST || rightmostCellRight == -Infinity ) {
if ( atST ) {
rightmostCellRight = lockScroll;
} else {
rightmostCellRight = BUFFER;
BUFFER += this.viewport.clientWidth;
}
this.allCellsToPool();
}
do {
pool = this.#colPool.shift();
if ( pool ) {
pool.left = rightmostCellRight;
this.#cols.push(pool);
i++;
pool.cells.forEach(cell => {
cell.classList.remove('sc-cell');
cell.classList.add('sc-item');
cell.style.top = 0;
cell.style.left = `${pool.left}px`;
cell.style.width = `${pool.width}px`;
});
rightmostCellRight += pool.width;
}
} while ( (i < 1 || atST) && pool && rightmostCellRight < BUFFER );
}
toPool(el) {
el.style.top = `-${el.clientHeight+10}px`;
el.classList.add('sc-pool');
el.classList.remove('sc-item');
}
toPoolCol(col) {
const top = this.viewport.scrollTop + this.viewport.clientHeight;
col.cells.forEach(cell => this.toPoolCell(cell, top));
this.#colPool.push(col);
}
toPoolCell(el, top) {
el.style.top = `-${top+el.clientHeight+150}px`;
el.classList.add('sc-pool');
el.classList.remove('sc-item');
}
poolToTop(atST = false) {
let BUFFER = Math.max(0, this.viewport.scrollTop - 50);
let i = 0, pool, highestRowTop;
highestRowTop = this.highestRowTop();
if ( atST || highestRowTop == Infinity ) {
if ( atST ) {
highestRowTop = this.viewport.scrollTop + this.viewport.clientHeight;
} else {
highestRowTop = BUFFER;
BUFFER -= this.viewport.clientHeight;
}
this.allToPool();
}
do {
pool = this.viewport.querySelector('tr.sc-pool');
if ( pool ) {
i++;
pool.classList.remove('sc-pool');
pool.classList.add('sc-item');
pool.style.top = `${highestRowTop-pool.clientHeight}px`;
highestRowTop -= pool.clientHeight;
}
} while( (i < 1 || atST) && pool && highestRowTop > BUFFER );
}
poolToBottom(atST = false) {
let BUFFER = this.viewport.scrollTop + this.viewport.clientHeight + 50;
let i = 0, pool, lowestRowBottom;
lowestRowBottom = this.lowestRowBottom();
if ( atST || lowestRowBottom == -Infinity ) {
if ( atST ) {
lowestRowBottom = this.viewport.scrollTop;
} else {
lowestRowBottom = BUFFER;
BUFFER += this.viewport.clientHeight;
}
this.allToPool();
}
do {
pool = this.viewport.querySelector('tr.sc-pool');
if ( pool ) {
i++;
pool.classList.remove('sc-pool');
pool.classList.add('sc-item');
pool.style.top = `${lowestRowBottom}px`;
lowestRowBottom += pool.clientHeight;
}
} while ( (i < 1 || atST) && pool && lowestRowBottom < BUFFER );
}
UpdatePosition(scrollEvent) {
if ( this.#updating ) return;
this.#updating = true;
const {target} = scrollEvent;
{
const thisScrollTop = target.scrollTop;
let dist = 0, needRejig = false;
if ( thisScrollTop !== this.#lastScrollTop ) {
dist = thisScrollTop - this.#lastScrollTop;
this.#ydirection = Math.sign(dist);
this.#lastScrollTop = thisScrollTop;
needRejig = Math.abs(dist) > this.viewport.clientHeight || (this.#lastYDirection !== this.#ydirection);
this.#lastYDirection = this.#ydirection;
if ( needRejig ) {
// we could debounce/throttle this on scroll
if ( this.#yrejig ) clearTimeout(this.#yrejig );
this.#yrejig = setTimeout(() => {
this.#yrejig = false;
this.allToPool();
if ( this.#ydirection > 0 ) {
this.poolToBottom(true);
} else {
this.poolToTop(true);
}
}, 50);
}
}
}
{
const thisScrollLeft = target.scrollLeft;
let span = 0, needRejigX = false;
if ( thisScrollLeft !== this.#lastScrollLeft ) {
span = thisScrollLeft - this.#lastScrollLeft;
this.#xdirection = Math.sign(span);
this.#lastScrollLeft = thisScrollLeft;
needRejigX = Math.abs(span) > this.viewport.clientWidth || (this.#lastXDirection !== this.#xdirection && this.#xdirection !== 0);
if ( needRejigX ) {
this.#lastXDirection = this.#xdirection;
// we could debounce/throttle this on scroll
if ( this.#xrejig ) clearTimeout(this.#xrejig );
this.#xrejig = setTimeout(() => {
this.#xrejig = false;
this.allCellsToPool();
const lockScroll = this.viewport.scrollLeft;
if ( this.#xdirection > 0 ) {
this.poolToRight(true, lockScroll);
} else {
this.poolToLeft(true, lockScroll);
}
}, 50);
}
}
}
this.#updating = false;
}
}