fix(frontend): improve performance of table-related utils

Fixes #603
This commit is contained in:
Gregor Kleen 2020-06-24 12:18:24 +02:00
parent c87315006d
commit eff273bf09
6 changed files with 191 additions and 87 deletions

View File

@ -0,0 +1,153 @@
const DEBUG_MODE = /localhost/.test(window.location.href) ? 1 : 0;
import * as defer from 'lodash.defer';
class Overhang {
colSpan;
rowSpan;
constructor(colSpan, rowSpan) {
this.colSpan = colSpan;
this.rowSpan = rowSpan;
if (new.target === Overhang)
Object.freeze(this);
}
nextLine() {
return new Overhang(this.colSpan, Math.max(0, this.rowSpan - 1));
}
isHole() {
return this.rowSpan <= 0;
}
}
const instanceCache = new Map();
export class TableIndices {
_table;
_cellToIndices = new Map();
_indicesToCell = new Array();
colSpan = cell => cell ? Math.max(1, cell.colSpan || 1) : 1;
rowSpan = cell => cell ? Math.max(1, cell.rowSpan || 1) : 1;
maxRow = 0;
maxCol = 0;
constructor(table, overrides) {
const prev = instanceCache.get(table);
if ( prev?.instance &&
overrides?.colSpan === prev.overrides?.colSpan &&
overrides?.rowSpan === prev.overrides?.rowSpan
) {
if (DEBUG_MODE > 0)
console.log('Reusing existing TableIndices', table, overrides, prev);
return prev.instance;
}
if (overrides && overrides.colSpan)
this.colSpan = overrides.colSpan;
if (overrides && overrides.rowSpan)
this.rowSpan = overrides.rowSpan;
this._table = table;
let overhangs = new Array();
let currentRow = 0;
for (const rowParent of this._table.rows) {
let currentOverhangs = Array.from(overhangs);
let newOverhangs = new Array();
let cellBefore = 0;
for (const cell of rowParent.cells) {
let i;
for (i = 0; i < currentOverhangs.length; i++) {
const overhang = currentOverhangs[i];
if (overhang.isHole())
break;
else
newOverhangs.push(overhang.nextLine());
cellBefore += overhang.colSpan;
}
currentOverhangs = currentOverhangs.slice(i);
this._cellToIndices.set(cell, { row: currentRow, col: cellBefore });
let rows = range(currentRow, currentRow + this.rowSpan(cell));
let columns = range(cellBefore, cellBefore + this.colSpan(cell));
if (DEBUG_MODE > 0) {
cell.dataset.rows = JSON.stringify(rows);
cell.dataset.columns = JSON.stringify(columns);
}
for (const row of rows) {
for (const col of columns) {
if (!this._indicesToCell[row])
this._indicesToCell[row] = new Array();
this._indicesToCell[row][col] = cell;
this.maxRow = Math.max(row, this.maxRow);
this.maxCol = Math.max(col, this.maxCol);
}
}
newOverhangs.push(new Overhang(this.colSpan(cell), this.rowSpan(cell) - 1));
cellBefore += this.colSpan(cell);
}
overhangs = newOverhangs;
currentRow++;
}
if (DEBUG_MODE > 1) {
console.log(this._cellToIndices);
console.table(this._indicesToCell);
}
instanceCache.set(table, { overrides: overrides, instance: this });
defer(() => { instanceCache.delete(table); } );
}
colIndex(cell) {
return this.getIndices(cell)?.col;
}
rowIndex(cell) {
return this.getIndices(cell)?.row;
}
getIndices(cell) {
const res = this._cellToIndices.get(cell);
if (DEBUG_MODE > 2)
console.log('getIndices', cell, res);
return res;
}
getCell(row, col) {
const cell = this._indicesToCell[row]?.[col];
if (DEBUG_MODE > 2)
console.log('getCell', row, col, cell);
return cell;
}
}
function range(from, to) {
return [...Array(to - from).keys()].map(n => n + from);
}

View File

@ -1,6 +1,6 @@
import * as toposort from 'toposort';
const DEBUG_MODE = /localhost/.test(window.location.href) ? 2 : 0;
const DEBUG_MODE = /localhost/.test(window.location.href) ? 1 : 0;
export class UtilRegistry {

View File

@ -1,4 +1,5 @@
import { Utility } from '../../core/utility';
import { TableIndices } from '../../lib/table/table';
const CHECKBOX_SELECTOR = '[type="checkbox"]';
@ -10,8 +11,10 @@ const CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized';
export class CheckAll {
_element;
_columns = [];
_checkAllColumns = [];
_columns = new Array();
_checkAllColumns = new Array();
_tableIndices;
constructor(element, app) {
if (!element) {
@ -24,6 +27,8 @@ export class CheckAll {
return false;
}
this._tableIndices = new TableIndices(this._element);
this._gatherColumns();
this._findCheckboxColumns().forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId])));
@ -32,28 +37,23 @@ export class CheckAll {
}
_gatherColumns() {
const rows = Array.from(this._element.rows);
const cols = [];
rows.forEach((tr) => {
const cells = Array.from(tr.cells);
cells.forEach(cell => {
let i = 0;
for (const sibling of cells.slice(0, cell.cellIndex))
i += Math.max(1, sibling.colSpan) || 1;
for (const rowIndex of new Array(this._tableIndices.maxRow + 1)) {
for (const colIndex of new Array(this._tableIndices.maxCol + 1)) {
const cell = this._tableIndices.getCell(rowIndex, colIndex);
for (let j = i; j < i + cell.colSpan; j++) {
if (!cols[j]) {
cols[j] = [];
}
cols[j].push(cell);
}
});
});
this._columns = cols;
if (!cell)
continue;
if (!this._columns[colIndex])
this._columns[colIndex] = new Array();
this._columns[colIndex][rowIndex] = cell;
}
}
}
_findCheckboxColumns() {
let checkboxColumnIds = [];
let checkboxColumnIds = new Array();
this._columns.forEach((col, i) => {
if (this._isCheckboxColumn(col)) {
checkboxColumnIds.push(i);

View File

@ -1,7 +1,8 @@
import { Utility } from '../../core/utility';
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
import './hide-columns.sass';
import * as memoize from 'lodash.memoize';
import { TableIndices } from '../../lib/table/table';
const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns';
const TABLE_HEADER_IDENT = 'uw-hide-column-header';
@ -37,6 +38,8 @@ export class HideColumns {
_mutationObserver;
_tableIndices;
headerToHider = new Map();
hiderToHeader = new Map();
@ -60,6 +63,8 @@ export class HideColumns {
this._element = element;
this._tableIndices = new TableIndices(this._element);
const hideColumnsContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}]`);
if (!hideColumnsContainer) {
throw new Error('Hide Columns utility needs to be setup on a table inside a hide columns container!');
@ -250,8 +255,7 @@ export class HideColumns {
// console.debug('_tableMutated', { mutationList });
this._colIndexMemoized = undefined;
this._getColMemoized = undefined;
this._tableIndices = new TableIndices(this._element, { colSpan: this.colSpan.bind(this) });
Array.from(this._element.rows)
.flatMap(row => Array.from(row.cells))
@ -354,71 +358,12 @@ export class HideColumns {
return originalColspan ? Math.max(colspan, originalColspan) : colspan;
}
colIndexDirect(cell) {
if (!cell)
return 0;
const rowParent = cell.closest('tr');
if (!rowParent)
return 0;
let rowBefore = 0;
for (const cell of Array.from(rowParent.cells).slice(0, cell.cellIndex)) {
if (!cell)
continue;
rowBefore += this.colSpan(cell);
}
let i = 0;
for (const pRow of Array.from(this._element.rows).slice(0, rowParent.rowIndex + 1)) {
if (!pRow)
continue;
let space = 0;
for (const cell of pRow === rowParent ? Array.from(pRow.cells).slice(0, cell.cellIndex) : pRow.cells) {
if (!cell)
continue;
const rowSpan = cell.rowSpan || 1;
if (rowParent.rowIndex - pRow.rowIndex < rowSpan)
i += this.colSpan(cell);
else if (pRow !== rowParent)
space += this.colSpan(cell);
if (space > rowBefore)
break;
}
}
// console.debug({ rowParent, cell, rowBefore, i });
return i;
}
_colIndexMemoized;
colIndex(cell) {
if (!this._colIndexMemoized)
this._colIndexMemoized = memoize(this.colIndexDirect.bind(this));
return this._colIndexMemoized(cell);
return this._tableIndices.colIndex(cell);
}
getColDirect(row, columnIndex) {
for (const cell of row.cells)
if (cell && this.colIndex(cell) <= columnIndex && this.colIndex(cell) + this.colSpan(cell) > columnIndex)
return cell;
}
_getColMemoized;
getCol(row, columnIndex) {
if (!this._getColMemoized)
this._getColMemoized = memoize(this.getColDirect.bind(this), (row, columnIndex) => Array.of(row.rowIndex, columnIndex).toString());
return this._getColMemoized(row, columnIndex);
getCol(row, col) {
return this._tableIndices.getCell(row.rowIndex, col);
}
}

5
package-lock.json generated
View File

@ -10952,6 +10952,11 @@
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=",
"dev": true
},
"lodash.defer": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/lodash.defer/-/lodash.defer-4.1.0.tgz",
"integrity": "sha1-6cFYqWHeGkbqJP2jRoW0zN01jz8="
},
"lodash.filter": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz",

View File

@ -89,7 +89,6 @@
"karma-mocha-reporter": "^2.2.5",
"karma-webpack": "^3.0.5",
"lint-staged": "^8.2.1",
"lodash.debounce": "^4.0.8",
"mini-css-extract-plugin": "^0.8.2",
"npm-run-all": "^4.1.5",
"null-loader": "^2.0.0",
@ -120,6 +119,8 @@
"@juggle/resize-observer": "^2.5.0",
"core-js": "^3.6.5",
"js-cookie": "^2.2.1",
"lodash.debounce": "^4.0.8",
"lodash.defer": "^4.1.0",
"lodash.throttle": "^4.1.1",
"moment": "^2.25.3",
"npm": "^6.14.5",