@serenity-is/sleekgrid
Version:
A modern Data Grid / Spreadsheet component
399 lines (345 loc) • 12.9 kB
text/typescript
import { Column, H, parsePx, Position } from "../core";
// shared across all grids on the page
let maxSupportedCssHeight: number; // browser's breaking point
let scrollbarDimensions: { width: number, height: number };
export function absBox(elem: HTMLElement): Position {
var box: Position = {
top: elem.offsetTop,
left: elem.offsetLeft,
bottom: 0,
right: 0,
width: elem.offsetWidth,
height: elem.offsetHeight,
visible: true
};
box.bottom = box.top + box.height;
box.right = box.left + box.width;
// walk up the tree
var offsetParent = elem.offsetParent;
while ((elem = elem.parentNode as HTMLElement) != document.body && elem != null) {
if (box.visible && elem.scrollHeight != elem.offsetHeight && getComputedStyle(elem).overflowY !== "visible") {
box.visible = box.bottom > elem.scrollTop && box.top < elem.scrollTop + elem.clientHeight;
}
if (box.visible && elem.scrollWidth != elem.offsetWidth && getComputedStyle(elem).overflowX != "visible") {
box.visible = box.right > elem.scrollLeft && box.left < elem.scrollLeft + elem.clientWidth;
}
box.left -= elem.scrollLeft;
box.top -= elem.scrollTop;
if (elem === offsetParent) {
box.left += elem.offsetLeft;
box.top += elem.offsetTop;
offsetParent = elem.offsetParent;
}
box.bottom = box.top + box.height;
box.right = box.left + box.width;
}
return box;
}
export function autosizeColumns(cols: Column[], availWidth: number, absoluteColMinWidth: number): boolean {
var i, c,
widths = [],
shrinkLeeway = 0,
total = 0,
prevTotal;
for (i = 0; i < cols.length; i++) {
c = cols[i];
widths.push(c.width);
total += c.width;
if (c.resizable) {
shrinkLeeway += c.width - Math.max(c.minWidth, absoluteColMinWidth);
}
}
// shrink
prevTotal = total;
while (total > availWidth && shrinkLeeway) {
var shrinkProportion = (total - availWidth) / shrinkLeeway;
for (i = 0; i < cols.length && total > availWidth; i++) {
c = cols[i];
var width = widths[i];
if (!c.resizable || width <= c.minWidth || width <= absoluteColMinWidth) {
continue;
}
var absMinWidth = Math.max(c.minWidth, absoluteColMinWidth);
var shrinkSize = Math.floor(shrinkProportion * (width - absMinWidth)) || 1;
shrinkSize = Math.min(shrinkSize, width - absMinWidth);
total -= shrinkSize;
shrinkLeeway -= shrinkSize;
widths[i] -= shrinkSize;
}
if (prevTotal <= total) { // avoid infinite loop
break;
}
prevTotal = total;
}
// grow
prevTotal = total;
while (total < availWidth) {
var growProportion = availWidth / total;
for (i = 0; i < cols.length && total < availWidth; i++) {
c = cols[i];
var currentWidth = widths[i];
var growSize;
if (!c.resizable || c.maxWidth <= currentWidth) {
growSize = 0;
} else {
growSize = Math.min(Math.floor(growProportion * currentWidth) - currentWidth, (c.maxWidth - currentWidth) || 1000000) || 1;
}
total += growSize;
widths[i] += (total <= availWidth ? growSize : 0);
}
if (prevTotal >= total) { // avoid infinite loop
break;
}
prevTotal = total;
}
var reRender = false;
for (i = 0; i < cols.length; i++) {
if (cols[i].rerenderOnResize && cols[i].width != widths[i]) {
reRender = true;
}
cols[i].width = widths[i];
}
return reRender;
}
export function getMaxSupportedCssHeight(recalc?: boolean): number {
if (!recalc && maxSupportedCssHeight != null)
return maxSupportedCssHeight;
return (maxSupportedCssHeight = ((navigator.userAgent.toLowerCase().match(/gecko\//) ? 4000000 : 32000000)));
}
export function getScrollBarDimensions(recalc?: boolean): { width: number; height: number; } {
if (!scrollbarDimensions || recalc) {
var c = document.body.appendChild(H('div', {
style: 'position:absolute;top:-10000px;left:-10000px;width:100px;height:100px;overflow: scroll;border:0'
}));
scrollbarDimensions = {
width: Math.round(c.offsetWidth - c.clientWidth),
height: Math.round(c.offsetWidth - c.clientHeight)
};
c.remove();
}
return scrollbarDimensions;
}
export function simpleArrayEquals(arr1: number[], arr2: number[]) {
if (!Array.isArray(arr1) || !Array.isArray(arr2) || arr1.length !== arr2.length)
return false;
arr1 = arr1.slice().sort();
arr2 = arr2.slice().sort();
for (var i = 0; i < arr1.length; i++) {
if (arr1[i] !== arr2[i])
return false;
}
return true;
}
/**
* Helper to sort visible cols, while keeping invisible cols sticky to
* the previous visible col. For example, if columns are currently in order
* A, B, C, D, E, F, G, H and desired order is G, D, F (assuming A, B, C, E
* were invisible) the result is A, B, G, H, D, E, F.
*/
export function sortToDesiredOrderAndKeepRest(columns: Column[], idOrder: string[]): Column[] {
if (idOrder.length == 0)
return columns;
var orderById: { [key: string]: number } = {},
colIdxById: { [key: string]: number } = {},
result: Column[] = [];
for (var i = 0; i < idOrder.length; i++)
orderById[idOrder[i]] = i;
for (i = 0; i < columns.length; i++)
colIdxById[columns[i].id] = i;
function takeFrom(i: number) {
for (var j = i; j < columns.length; j++) {
var c = columns[j];
if (i != j && orderById[c.id] != null)
break;
result.push(c);
colIdxById[c.id] = null;
}
}
if (orderById[columns[0].id] == null)
takeFrom(0);
for (var id of idOrder) {
i = colIdxById[id];
if (i != null)
takeFrom(i);
}
for (i = 0; i < columns.length; i++) {
var c = columns[i];
if (colIdxById[c.id] != null) {
result.push(c);
colIdxById[c.id] = null;
}
}
return result;
}
export function calcMinMaxPageXOnDragStart(cols: Column[], colIdx: number, pageX: number, forceFit: boolean, absoluteColMinWidth: number): { maxPageX: number; minPageX: number; } {
var shrinkLeewayOnRight = null, stretchLeewayOnRight = null, j: number, c: Column;
if (forceFit) {
shrinkLeewayOnRight = 0;
stretchLeewayOnRight = 0;
// colums on right affect maxPageX/minPageX
for (j = colIdx + 1; j < cols.length; j++) {
c = cols[j];
if (c.resizable) {
if (stretchLeewayOnRight != null) {
if (c.maxWidth) {
stretchLeewayOnRight += c.maxWidth - c.previousWidth;
} else {
stretchLeewayOnRight = null;
}
}
shrinkLeewayOnRight += c.previousWidth - Math.max(c.minWidth || 0, absoluteColMinWidth);
}
}
}
var shrinkLeewayOnLeft = 0, stretchLeewayOnLeft = 0;
for (j = 0; j <= colIdx; j++) {
// columns on left only affect minPageX
c = cols[j];
if (c.resizable) {
if (stretchLeewayOnLeft != null) {
if (c.maxWidth) {
stretchLeewayOnLeft += c.maxWidth - c.previousWidth;
} else {
stretchLeewayOnLeft = null;
}
}
shrinkLeewayOnLeft += c.previousWidth - Math.max(c.minWidth || 0, absoluteColMinWidth);
}
}
if (shrinkLeewayOnRight === null) {
shrinkLeewayOnRight = 100000;
}
if (shrinkLeewayOnLeft === null) {
shrinkLeewayOnLeft = 100000;
}
if (stretchLeewayOnRight === null) {
stretchLeewayOnRight = 100000;
}
if (stretchLeewayOnLeft === null) {
stretchLeewayOnLeft = 100000;
}
return {
maxPageX: pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft),
minPageX: pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight)
}
}
export function shrinkOrStretchColumn(cols: Column[], colIdx: number, d: number, forceFit: boolean, absoluteColMinWidth: number): void {
var c: Column, j: number, x: number, actualMinWidth: number;
if (d < 0) { // shrink column
x = d;
for (j = colIdx; j >= 0; j--) {
c = cols[j];
if (c.resizable) {
actualMinWidth = Math.max(c.minWidth || 0, absoluteColMinWidth);
if (x && c.previousWidth + x < actualMinWidth) {
x += c.previousWidth - actualMinWidth;
c.width = actualMinWidth;
} else {
c.width = c.previousWidth + x;
x = 0;
}
}
}
if (forceFit) {
x = -d;
for (j = colIdx + 1; j < cols.length; j++) {
c = cols[j];
if (c.resizable) {
if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
x -= c.maxWidth - c.previousWidth;
c.width = c.maxWidth;
} else {
c.width = c.previousWidth + x;
x = 0;
}
}
}
}
} else { // stretch column
x = d;
for (j = colIdx; j >= 0; j--) {
c = cols[j];
if (c.resizable) {
if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
x -= c.maxWidth - c.previousWidth;
c.width = c.maxWidth;
} else {
c.width = c.previousWidth + x;
x = 0;
}
}
}
if (forceFit) {
x = -d;
for (j = colIdx + 1; j < cols.length; j++) {
c = cols[j];
if (c.resizable) {
actualMinWidth = Math.max(c.minWidth || 0, absoluteColMinWidth);
if (x && c.previousWidth + x < actualMinWidth) {
x += c.previousWidth - actualMinWidth;
c.width = actualMinWidth;
} else {
c.width = c.previousWidth + x;
x = 0;
}
}
}
}
}
}
export function addUiStateHover() {
(this as HTMLElement)?.classList.add("ui-state-hover");
}
export function removeUiStateHover() {
(this as HTMLElement)?.classList.remove("ui-state-hover");
}
export function getVBoxDelta(el: HTMLElement): number {
if (!el)
return 0;
var style = getComputedStyle(el);
if (style.boxSizing === 'border-box')
return 0;
var p = ["border-top-width", "border-bottom-width", "padding-top", "padding-bottom"];
var delta = 0;
for (var val of p)
delta += parsePx(style.getPropertyValue(val)) || 0;
return delta;
}
export function getInnerWidth(el: HTMLElement): number {
var style = getComputedStyle(el);
var width = parsePx(style.width) ?? 0;
if (style.boxSizing != 'border-box')
return Math.max(0, width);
var p = ["border-left-width", "border-right-width", "padding-left", "padding-right"];
for (var val of p)
width -= parsePx(style.getPropertyValue(val)) || 0;
return Math.max(width, 0);
}
export interface CachedRow {
rowNodeL: HTMLElement,
rowNodeR: HTMLElement,
// ColSpans of rendered cells (by column idx).
// Can also be used for checking whether a cell has been rendered.
cellColSpans: number[],
// Cell nodes (by column idx). Lazy-populated by ensureCellNodesInRowsCache().
cellNodesByColumnIdx: { [key: number]: HTMLElement },
// Column indices of cell nodes that have been rendered, but not yet indexed in
// cellNodesByColumnIdx. These are in the same order as cell nodes added at the
// end of the row.
cellRenderQueue: number[];
// Elements returned from formatters for cells in cellRenderQueue.
cellRenderContent: (Element | DocumentFragment)[];
}
export interface GoToResult {
row: number;
cell: number;
posX: number;
}
export interface PostProcessCleanupEntry {
groupId: number,
cellNode?: HTMLElement,
columnIdx?: number,
rowNodeL?: HTMLElement;
rowNodeR?: HTMLElement;
rowIdx?: number;
}