mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-28 01:25:39 -05:00
Better table implementation (#11781)
* wip * wip * weird splitview scrolling stuff * working table * remove spliceable table * handling resizing columns * get perf table integrated into grid * make more improvments to table view * testing * wip * wip * fix async data window; add more optimization to scrolling * work on scrolling * fix column resizing * start working on table widget * inital work to get table widget working with styles and mouse controls * fix unrendering selection; fix sizes of cells * support high perf table option; remove unused files; add cell borders to high perf * add accessibility tags * handle borders and row count * more styling changfes * fix strict null checks * adding inital keyboard navigation * center row count; add padding left to rows * inital drag selection * remove drag implementation; it can be done better utilizing the global mouse monitor object * range logic * create custom grid range * work with new range * remove unused code * fix how plus range works * add drag selection; change focus to set selection; fix problem with creating a range with inverse start and end * code cleanup * fix strict-null-checks * fix up perf table * fix layering * inital table service * finish table service * fix some compile errors * fix compile * fix compile * fix up for use * fix layering * remove console use * fix strict nulls
This commit is contained in:
97
src/sql/base/browser/ui/table/highPerf/cellCache.ts
Normal file
97
src/sql/base/browser/ui/table/highPerf/cellCache.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ITableRenderer } from 'sql/base/browser/ui/table/highPerf/table';
|
||||
|
||||
import { $, removeClass } from 'vs/base/browser/dom';
|
||||
export interface ICell {
|
||||
domNode: HTMLElement | null;
|
||||
templateData: any;
|
||||
templateId: string;
|
||||
}
|
||||
|
||||
function removeFromParent(element: HTMLElement): void {
|
||||
try {
|
||||
if (element.parentElement) {
|
||||
element.parentElement.removeChild(element);
|
||||
}
|
||||
} catch (e) {
|
||||
// this will throw if this happens due to a blur event, nasty business
|
||||
}
|
||||
}
|
||||
|
||||
export class CellCache<T> implements IDisposable {
|
||||
|
||||
private cache = new Map<string, ICell[]>();
|
||||
|
||||
constructor(private renderers: Map<string, ITableRenderer<T, any>>) { }
|
||||
|
||||
alloc(templateId: string): ICell {
|
||||
let result = this.getTemplateCache(templateId).pop();
|
||||
|
||||
if (!result) {
|
||||
const domNode = $('.monaco-perftable-cell');
|
||||
const renderer = this.getRenderer(templateId);
|
||||
const templateData = renderer.renderTemplate(domNode);
|
||||
result = { domNode, templateId, templateData };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getTemplateCache(templateId: string): ICell[] {
|
||||
let result = this.cache.get(templateId);
|
||||
|
||||
if (!result) {
|
||||
result = [];
|
||||
this.cache.set(templateId, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getRenderer(templateId: string): ITableRenderer<T, any> {
|
||||
const renderer = this.renderers.get(templateId);
|
||||
if (!renderer) {
|
||||
throw new Error(`No renderer found for ${templateId}`);
|
||||
}
|
||||
return renderer;
|
||||
}
|
||||
|
||||
release(cell: ICell) {
|
||||
const { domNode, templateId } = cell;
|
||||
if (domNode) {
|
||||
removeClass(domNode, 'scrolling');
|
||||
removeFromParent(domNode);
|
||||
}
|
||||
|
||||
const cache = this.getTemplateCache(templateId);
|
||||
cache.push(cell);
|
||||
}
|
||||
|
||||
private garbageCollect(): void {
|
||||
if (!this.renderers) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cache.forEach((cachedRows, templateId) => {
|
||||
for (const cachedRow of cachedRows) {
|
||||
const renderer = this.getRenderer(templateId);
|
||||
renderer.disposeTemplate(cachedRow.templateData);
|
||||
cachedRow.domNode = null;
|
||||
cachedRow.templateData = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.garbageCollect();
|
||||
this.cache.clear();
|
||||
this.renderers = null!; // StrictNullOverride: nulling out ok in dispose
|
||||
}
|
||||
}
|
||||
17
src/sql/base/browser/ui/table/highPerf/rowCache.ts
Normal file
17
src/sql/base/browser/ui/table/highPerf/rowCache.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
|
||||
export class RowCache implements IDisposable {
|
||||
dispose(): void {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export interface IRow {
|
||||
domNode: HTMLElement | null;
|
||||
}
|
||||
106
src/sql/base/browser/ui/table/highPerf/table.css
Normal file
106
src/sql/base/browser/ui/table/highPerf/table.css
Normal file
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-perftable {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-perftable.mouse-support {
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: -moz-none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.monaco-perftable > .monaco-scrollable-element {
|
||||
height: calc(100% - 22px);
|
||||
}
|
||||
|
||||
.monaco-perftable .monaco-perftable-cell {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-perftable-rows {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-perftable.horizontal-scrolling .monaco-perftable-rows {
|
||||
width: auto;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.monaco-perftable-row {
|
||||
position: absolute;
|
||||
-moz-box-sizing: border-box;
|
||||
-o-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-perftable.mouse-support .monaco-perftable-row {
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/* for OS X balperftableic scrolling */
|
||||
.monaco-perftable-row.scrolling {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Focus */
|
||||
.monaco-perftable.element-focused, .monaco-perftable.selection-single, .monaco-perftable.selection-multiple {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
.monaco-perftable-header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-perftable-header-cell {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
padding-left: 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-perftable .sash-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.monaco-perftable .sash-container > .monaco-sash {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.monaco-perftable .monaco-perftable-cell * {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
.monaco-perftable .monaco-perftable-cell .row-count {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
96
src/sql/base/browser/ui/table/highPerf/table.ts
Normal file
96
src/sql/base/browser/ui/table/highPerf/table.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IGridRange } from 'sql/base/common/gridRange';
|
||||
import { IGridPosition } from 'sql/base/common/gridPosition';
|
||||
|
||||
export interface ITableRenderer<T, TTemplateData> {
|
||||
renderTemplate(container: HTMLElement): TTemplateData;
|
||||
renderCell(element: T, index: number, cell: number, columnId: string, templateData: TTemplateData, width: number | undefined): void;
|
||||
disposeCell?(element: T, index: number, cell: number, olumnId: string, templateData: TTemplateData, width: number | undefined): void;
|
||||
disposeTemplate(templateData: TTemplateData): void;
|
||||
}
|
||||
|
||||
export interface IStaticTableRenderer<T, TTemplateData> extends ITableRenderer<T, TTemplateData> {
|
||||
renderCell(element: T | undefined, index: number, cell: number, columnId: string, templateData: TTemplateData, width: number | undefined): void;
|
||||
disposeCell?(element: T | undefined, index: number, cell: number, columnId: string, templateData: TTemplateData, width: number | undefined): void;
|
||||
}
|
||||
|
||||
export class TableError extends Error {
|
||||
|
||||
constructor(user: string, message: string) {
|
||||
super(`TableError [${user}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITableDataSource<T> {
|
||||
getRow(index: number): Promise<T>;
|
||||
}
|
||||
|
||||
export interface ITableEvent<T> {
|
||||
elements: T[];
|
||||
indexes: IGridRange[];
|
||||
browserEvent?: UIEvent;
|
||||
}
|
||||
|
||||
export interface ITableMouseEvent<T> {
|
||||
browserEvent: MouseEvent;
|
||||
buttons: number;
|
||||
element: T | undefined;
|
||||
index: IGridPosition | undefined;
|
||||
}
|
||||
|
||||
export interface ITableContextMenuEvent<T> {
|
||||
browserEvent: UIEvent;
|
||||
element: T | undefined;
|
||||
index: IGridPosition | undefined;
|
||||
anchor: HTMLElement | { x: number; y: number; };
|
||||
}
|
||||
|
||||
export interface ITableDragEvent {
|
||||
start: IGridPosition;
|
||||
current: IGridPosition;
|
||||
}
|
||||
|
||||
export interface ITableColumn<T, TTemplateData> {
|
||||
/**
|
||||
* Renderer associated with this column
|
||||
*/
|
||||
renderer: ITableRenderer<T, TTemplateData> | IStaticTableRenderer<T, TTemplateData>;
|
||||
/**
|
||||
* Initial width of this column
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* Minimum allowed width of this column
|
||||
*/
|
||||
minWidth?: number;
|
||||
/**
|
||||
* Is this column resizable?
|
||||
*/
|
||||
resizeable?: boolean;
|
||||
/**
|
||||
* This string will be added to the cell as a class
|
||||
* Useful for styling specific columns
|
||||
*/
|
||||
cellClass?: string;
|
||||
/**
|
||||
* Specifies this column doesn't need data to render
|
||||
* Useful when you don't need to wait for data you render a column
|
||||
*/
|
||||
static?: boolean;
|
||||
id: string;
|
||||
/**
|
||||
* Name to display in the column header
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IStaticColumn<T, TTemplateData> extends ITableColumn<T, TTemplateData> {
|
||||
/**
|
||||
* Renderer associated with this column
|
||||
*/
|
||||
renderer: IStaticTableRenderer<T, TTemplateData>;
|
||||
}
|
||||
727
src/sql/base/browser/ui/table/highPerf/tableView.ts
Normal file
727
src/sql/base/browser/ui/table/highPerf/tableView.ts
Normal file
@@ -0,0 +1,727 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./table';
|
||||
|
||||
import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { ScrollEvent, ScrollbarVisibility, INewScrollDimensions } from 'vs/base/common/scrollable';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import { Range, IRange } from 'vs/base/common/range';
|
||||
import { getOrDefault } from 'vs/base/common/objects';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { Sash, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash';
|
||||
import { firstIndex } from 'vs/base/common/arrays';
|
||||
|
||||
import { CellCache, ICell } from 'sql/base/browser/ui/table/highPerf/cellCache';
|
||||
import { ITableRenderer, ITableDataSource, ITableMouseEvent, IStaticTableRenderer, ITableColumn } from 'sql/base/browser/ui/table/highPerf/table';
|
||||
import { GridPosition } from 'sql/base/common/gridPosition';
|
||||
|
||||
export interface IAriaSetProvider<T> {
|
||||
getSetSize(element: T, index: number, listLength: number): number;
|
||||
getPosInSet(element: T, index: number): number;
|
||||
}
|
||||
|
||||
export interface ITableViewOptions<T> {
|
||||
rowHeight?: number;
|
||||
mouseSupport?: boolean;
|
||||
initialLength?: number;
|
||||
rowCountColumn?: boolean;
|
||||
headerHeight?: number;
|
||||
}
|
||||
|
||||
const DefaultOptions = {
|
||||
rowHeight: 22,
|
||||
columnWidth: 120,
|
||||
minWidth: 20,
|
||||
resizeable: true,
|
||||
headerHeight: 22
|
||||
};
|
||||
|
||||
interface IInternalColumn<T, TTemplateData> extends ITableColumn<T, TTemplateData> {
|
||||
domNode?: HTMLElement;
|
||||
left?: number;
|
||||
}
|
||||
|
||||
interface IInternalStaticColumn<T, TTemplateData> extends IInternalColumn<T, TTemplateData> {
|
||||
renderer: IStaticTableRenderer<T, TTemplateData>;
|
||||
}
|
||||
|
||||
interface ISashItem {
|
||||
sash: Sash;
|
||||
disposable: IDisposable;
|
||||
}
|
||||
|
||||
interface ISashDragState {
|
||||
current: number;
|
||||
index: number;
|
||||
start: number;
|
||||
sizes: Array<number>;
|
||||
lefts: Array<number>;
|
||||
}
|
||||
|
||||
interface ISashEvent<T> {
|
||||
sash: Sash;
|
||||
column: IInternalColumn<T, any>;
|
||||
start: number;
|
||||
current: number;
|
||||
}
|
||||
|
||||
function removeFromParent(element: HTMLElement): void {
|
||||
try {
|
||||
if (element.parentElement) {
|
||||
element.parentElement.removeChild(element);
|
||||
}
|
||||
} catch (e) {
|
||||
// this will throw if this happens due to a blur event, nasty business
|
||||
}
|
||||
}
|
||||
|
||||
interface IAsyncRowItem<T> {
|
||||
readonly id: string;
|
||||
element: T | undefined;
|
||||
row: HTMLElement | null;
|
||||
cells: ICell[] | null;
|
||||
size: number;
|
||||
datapromise: CancelablePromise<void> | null;
|
||||
}
|
||||
|
||||
export class TableView<T> implements IDisposable {
|
||||
private static InstanceCount = 0;
|
||||
readonly domId = `table_id_${++TableView.InstanceCount}`;
|
||||
|
||||
readonly domNode = DOM.$('.monaco-perftable');
|
||||
|
||||
private visibleRows: IAsyncRowItem<T>[] = [];
|
||||
private cache: CellCache<T>;
|
||||
private renderers = new Map<string, ITableRenderer<T /* TODO@joao */, any>>();
|
||||
private lastRenderTop = 0;
|
||||
private lastRenderHeight = 0;
|
||||
private readonly rowsContainer = DOM.$('.monaco-perftable-rows');
|
||||
private scrollableElement: ScrollableElement;
|
||||
private _scrollHeight: number = 0;
|
||||
private _scrollWidth: number = 0;
|
||||
private scrollableElementUpdateDisposable: IDisposable | null = null;
|
||||
// private ariaSetProvider: IAriaSetProvider<T>;
|
||||
private canUseTranslate3d: boolean | undefined = undefined;
|
||||
public readonly rowHeight: number;
|
||||
private _length: number = 0;
|
||||
|
||||
private columns: IInternalColumn<T, any>[];
|
||||
private staticColumns: IInternalStaticColumn<T, any>[];
|
||||
private columnSashs: ISashItem[] = [];
|
||||
private sashDragState?: ISashDragState;
|
||||
private headerContainer!: HTMLElement;
|
||||
|
||||
private scheduledRender?: IDisposable;
|
||||
private bigNumberDelta = 0;
|
||||
|
||||
private headerHeight: number;
|
||||
|
||||
private disposables: IDisposable[];
|
||||
|
||||
get contentHeight(): number { return this.length * this.rowHeight; }
|
||||
|
||||
get onDidScroll(): Event<ScrollEvent> { return this.scrollableElement.onScroll; }
|
||||
|
||||
private _orthogonalStartSash: Sash | undefined;
|
||||
get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; }
|
||||
set orthogonalStartSash(sash: Sash | undefined) {
|
||||
for (const sashItem of this.columnSashs) {
|
||||
sashItem.sash.orthogonalStartSash = sash;
|
||||
}
|
||||
|
||||
this._orthogonalStartSash = sash;
|
||||
}
|
||||
|
||||
private _orthogonalEndSash: Sash | undefined;
|
||||
get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; }
|
||||
set orthogonalEndSash(sash: Sash | undefined) {
|
||||
for (const sashItem of this.columnSashs) {
|
||||
sashItem.sash.orthogonalEndSash = sash;
|
||||
}
|
||||
|
||||
this._orthogonalEndSash = sash;
|
||||
}
|
||||
|
||||
get sashes(): Sash[] {
|
||||
return this.columnSashs.map(s => s.sash);
|
||||
}
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
columns: ITableColumn<T, any>[],
|
||||
private readonly dataSource: ITableDataSource<T>,
|
||||
options: ITableViewOptions<T> = DefaultOptions as ITableViewOptions<T>,
|
||||
) {
|
||||
for (const column of columns) {
|
||||
this.renderers.set(column.id, column.renderer);
|
||||
}
|
||||
|
||||
this.columns = columns.slice();
|
||||
this.staticColumns = this.columns.filter(c => c.static);
|
||||
|
||||
this.cache = new CellCache(this.renderers);
|
||||
|
||||
this.domNode.setAttribute('role', 'grid');
|
||||
this.domNode.setAttribute('aria-rowcount', '0');
|
||||
this.domNode.setAttribute('aria-readonly', 'true');
|
||||
|
||||
DOM.addClass(this.domNode, this.domId);
|
||||
this.domNode.tabIndex = 0;
|
||||
|
||||
DOM.toggleClass(this.domNode, 'mouse-support', typeof options.mouseSupport === 'boolean' ? options.mouseSupport : true);
|
||||
|
||||
// this.ariaSetProvider = { getSetSize: (e, i, length) => length, getPosInSet: (_, index) => index + 1 };
|
||||
|
||||
this.rowHeight = getOrDefault(options, o => o.rowHeight, DefaultOptions.rowHeight);
|
||||
this.headerHeight = getOrDefault(options, o => o.headerHeight, DefaultOptions.headerHeight);
|
||||
|
||||
let left = 0;
|
||||
this.columns = this.columns.map(c => {
|
||||
c.width = getOrDefault(c, o => o.width, DefaultOptions.columnWidth);
|
||||
c.minWidth = getOrDefault(c, o => o.minWidth, DefaultOptions.minWidth);
|
||||
c.resizeable = getOrDefault(c, c => c.resizeable, DefaultOptions.resizeable);
|
||||
c.left = left;
|
||||
left += c.width;
|
||||
return c;
|
||||
});
|
||||
|
||||
this.rowsContainer.setAttribute('role', 'rowgroup');
|
||||
|
||||
this.scrollableElement = new ScrollableElement(this.rowsContainer, {
|
||||
horizontal: ScrollbarVisibility.Auto,
|
||||
vertical: ScrollbarVisibility.Auto,
|
||||
useShadows: true
|
||||
});
|
||||
|
||||
this.renderHeader(this.domNode);
|
||||
|
||||
this.domNode.appendChild(this.scrollableElement.getDomNode());
|
||||
container.appendChild(this.domNode);
|
||||
|
||||
this.disposables = [/*this.gesture,*/ this.scrollableElement, this.cache];
|
||||
|
||||
this.scrollableElement.onScroll(this.onScroll, this, this.disposables);
|
||||
|
||||
// Prevent the monaco-scrollable-element from scrolling
|
||||
// https://github.com/Microsoft/vscode/issues/44181
|
||||
domEvent(this.scrollableElement.getDomNode(), 'scroll')
|
||||
(e => (e.target as HTMLElement).scrollTop = 0, null, this.disposables);
|
||||
|
||||
this.updateScrollWidth();
|
||||
this.layout();
|
||||
}
|
||||
|
||||
private renderHeader(container: HTMLElement): void {
|
||||
this.headerContainer = DOM.append(container, DOM.$('.monaco-perftable-header'));
|
||||
const sashContainer = DOM.append(this.headerContainer, DOM.$('.sash-container'));
|
||||
this.headerContainer.style.height = this.headerHeight + 'px';
|
||||
this.headerContainer.style.lineHeight = this.headerHeight + 'px';
|
||||
this.headerContainer.setAttribute('role', 'rowgroup');
|
||||
const headerCellContainer = DOM.append(this.headerContainer, DOM.$('.monaco-perftable-header-cell-container'));
|
||||
headerCellContainer.setAttribute('role', 'row');
|
||||
|
||||
for (const column of this.columns) {
|
||||
this.createHeaderSash(sashContainer, column);
|
||||
const domNode = DOM.append(headerCellContainer, DOM.$('.monaco-perftable-header-cell'));
|
||||
domNode.setAttribute('role', 'columnheader');
|
||||
domNode.style.width = column.width + 'px';
|
||||
domNode.style.left = column.left + 'px';
|
||||
domNode.innerText = column.name;
|
||||
column.domNode = domNode;
|
||||
}
|
||||
}
|
||||
|
||||
private createHeaderSash(sashContainer: HTMLElement, column: ITableColumn<T, any>): void {
|
||||
const layoutProvider = {
|
||||
getVerticalSashLeft: (sash: Sash) => {
|
||||
let left = 0;
|
||||
for (const c of this.columns) {
|
||||
left += c.width!;
|
||||
if (column === c) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
};
|
||||
const sash = new Sash(sashContainer, layoutProvider, {
|
||||
orientation: Orientation.VERTICAL,
|
||||
orthogonalStartSash: this.orthogonalStartSash,
|
||||
orthogonalEndSash: this.orthogonalEndSash
|
||||
});
|
||||
|
||||
const sashEventMapper = (e: IBaseSashEvent) => ({ sash, column, start: e.startX, current: e.currentX });
|
||||
|
||||
const onStart = Event.map(sash.onDidStart, sashEventMapper);
|
||||
const onStartDisposable = onStart(this.onSashStart, this);
|
||||
const onChange = Event.map(sash.onDidChange, sashEventMapper);
|
||||
const onChangeDisposable = onChange(this.onSashChange, this);
|
||||
// const onEnd = Event.map(sash.onDidEnd, () => firstIndex(this.columnSashs, item => item.sash === sash));
|
||||
// const onEndDisposable = onEnd(this.onSashEnd, this);
|
||||
// const onDidReset = Event.map(sash.onDidEnd, () => firstIndex(this.columnSashs, item => item.sash === sash));
|
||||
// const onDidResetDisposable = onDidReset(this.onDidSashReset, this);
|
||||
|
||||
const disposable = combinedDisposable(onStartDisposable, onChangeDisposable, /*onEndDisposable, onDidResetDisposable, */sash);
|
||||
const sashItem: ISashItem = { sash, disposable };
|
||||
this.columnSashs.push(sashItem);
|
||||
if (!column.resizeable) {
|
||||
sash.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private onSashStart({ sash, start }: ISashEvent<T>): void {
|
||||
const index = firstIndex(this.columnSashs, item => item.sash === sash);
|
||||
const sizes = this.columns.map(i => i.width!);
|
||||
const lefts = this.columns.map(i => i.left!);
|
||||
this.sashDragState = { start, current: start, index, sizes, lefts };
|
||||
}
|
||||
|
||||
private onSashChange({ column, current }: ISashEvent<T>): void {
|
||||
const { index, start, sizes, lefts } = this.sashDragState!;
|
||||
this.sashDragState!.current = current;
|
||||
|
||||
const delta = current - start;
|
||||
const adjustedDelta = sizes[index] + delta < column.minWidth! ? column.minWidth! - sizes[index] : delta;
|
||||
|
||||
column.width = sizes[index] + adjustedDelta;
|
||||
column.domNode!.style.width = column.width + 'px';
|
||||
for (let i = index + 1; i < this.columns.length; i++) {
|
||||
const resizeColumn = this.columns[i];
|
||||
resizeColumn.left = lefts[i] + adjustedDelta;
|
||||
resizeColumn.domNode!.style.left = resizeColumn.left + 'px';
|
||||
}
|
||||
for (const [index, row] of this.visibleRows.entries()) {
|
||||
if (row) {
|
||||
this.updateRowInDOM(row, index);
|
||||
}
|
||||
}
|
||||
this.updateScrollWidth();
|
||||
this.layout();
|
||||
}
|
||||
|
||||
private eventuallyUpdateScrollDimensions(): void {
|
||||
this._scrollHeight = this.contentHeight;
|
||||
this.rowsContainer.style.height = `${this._scrollHeight}px`;
|
||||
|
||||
if (!this.scrollableElementUpdateDisposable) {
|
||||
this.scrollableElementUpdateDisposable = DOM.scheduleAtNextAnimationFrame(() => {
|
||||
this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });
|
||||
this.updateScrollWidth();
|
||||
this.scrollableElementUpdateDisposable = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateScrollWidth(): void {
|
||||
this._scrollWidth = this.columns.reduce((p, c) => p += c.width!, 0);
|
||||
this.rowsContainer.style.width = `${this.scrollWidth}px`;
|
||||
this.headerContainer.style.width = `${this.scrollWidth}px`;
|
||||
this.scrollableElement.setScrollDimensions({ scrollWidth: this.scrollWidth });
|
||||
}
|
||||
|
||||
private onScroll(e: ScrollEvent): void {
|
||||
if (this.scheduledRender) {
|
||||
this.scheduledRender.dispose();
|
||||
}
|
||||
this.scheduledRender = DOM.runAtThisOrScheduleAtNextAnimationFrame(() => {
|
||||
this.render(e.scrollTop, e.height, e.scrollLeft, e.scrollWidth);
|
||||
});
|
||||
}
|
||||
|
||||
private getRenderRange(renderTop: number, renderHeight: number): IRange {
|
||||
const start = Math.floor(renderTop / this.rowHeight);
|
||||
const end = Math.ceil((renderTop + renderHeight) / this.rowHeight);
|
||||
return {
|
||||
start,
|
||||
end
|
||||
};
|
||||
}
|
||||
|
||||
private getNextToLastElement(ranges: IRange[]): HTMLElement | null {
|
||||
const lastRange = ranges[ranges.length - 1];
|
||||
|
||||
if (!lastRange) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextToLastItem = this.visibleRows[lastRange.end];
|
||||
|
||||
if (!nextToLastItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!nextToLastItem.row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return nextToLastItem.row;
|
||||
}
|
||||
|
||||
private render(renderTop: number, renderHeight: number, renderLeft: number, scrollWidth: number): void {
|
||||
const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
|
||||
const renderRange = this.getRenderRange(renderTop, renderHeight);
|
||||
renderRange.end = renderRange.end > this.length ? this.length : renderRange.end;
|
||||
|
||||
// IE (all versions) cannot handle units above about 1,533,908 px, so every 500k pixels bring numbers down
|
||||
const STEP_SIZE = 500000;
|
||||
this.bigNumberDelta = 0;
|
||||
if (renderTop >= STEP_SIZE) {
|
||||
// Compute a delta that guarantees that lines are positioned at `lineHeight` increments
|
||||
this.bigNumberDelta = Math.floor(renderTop / STEP_SIZE) * STEP_SIZE;
|
||||
this.bigNumberDelta = Math.floor(this.bigNumberDelta / this.rowHeight) * this.rowHeight;
|
||||
const rangesToUpdate = Range.intersect(previousRenderRange, renderRange);
|
||||
for (let i = rangesToUpdate.start; i < rangesToUpdate.end; i++) {
|
||||
this.updateRowInDOM(this.visibleRows[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
const rangesToInsert = Range.relativeComplement(renderRange, previousRenderRange);
|
||||
const rangesToRemove = Range.relativeComplement(previousRenderRange, renderRange);
|
||||
const beforeElement = this.getNextToLastElement(rangesToInsert);
|
||||
|
||||
for (const range of rangesToInsert) {
|
||||
for (let i = range.start; i < range.end; i++) {
|
||||
this.insertRowInDOM(i, beforeElement);
|
||||
}
|
||||
}
|
||||
|
||||
for (const range of rangesToRemove) {
|
||||
for (let i = range.start; i < range.end; i++) {
|
||||
this.removeRowFromDOM(i);
|
||||
}
|
||||
}
|
||||
|
||||
const canUseTranslate3d = !isWindows && !browser.isFirefox && browser.getZoomLevel() === 0;
|
||||
|
||||
if (canUseTranslate3d) {
|
||||
const transform = `translate3d(-${renderLeft}px, -${renderTop - this.bigNumberDelta}px, 0px)`;
|
||||
this.rowsContainer.style.transform = transform;
|
||||
this.rowsContainer.style.webkitTransform = transform;
|
||||
this.headerContainer.style.transform = `translate3d(-${renderLeft}px, 0px, 0px)`;
|
||||
this.headerContainer.style.webkitTransform = `translate3d(-${renderLeft}px, 0px, 0px)`;
|
||||
|
||||
if (canUseTranslate3d !== this.canUseTranslate3d) {
|
||||
this.rowsContainer.style.left = '0';
|
||||
this.headerContainer.style.left = '0';
|
||||
this.rowsContainer.style.top = '0';
|
||||
}
|
||||
} else {
|
||||
this.rowsContainer.style.left = `-${renderLeft}px`;
|
||||
this.headerContainer.style.left = `-${renderLeft}px`;
|
||||
this.rowsContainer.style.top = `-${renderTop - this.bigNumberDelta}px`;
|
||||
|
||||
if (canUseTranslate3d !== this.canUseTranslate3d) {
|
||||
this.rowsContainer.style.transform = '';
|
||||
this.rowsContainer.style.webkitTransform = '';
|
||||
}
|
||||
}
|
||||
|
||||
this.canUseTranslate3d = canUseTranslate3d;
|
||||
|
||||
this.lastRenderTop = renderTop;
|
||||
this.lastRenderHeight = renderHeight;
|
||||
}
|
||||
|
||||
public layout(height?: number, width?: number): void {
|
||||
const scrollDimensions: INewScrollDimensions = {
|
||||
height: typeof height === 'number' ? height : DOM.getContentHeight(this.domNode)
|
||||
};
|
||||
scrollDimensions.height = scrollDimensions.height! - this.headerHeight;
|
||||
if (this.scrollableElementUpdateDisposable) {
|
||||
this.scrollableElementUpdateDisposable.dispose();
|
||||
this.scrollableElementUpdateDisposable = null;
|
||||
scrollDimensions.scrollHeight = this.scrollHeight;
|
||||
}
|
||||
|
||||
this.scrollableElement.setScrollDimensions(scrollDimensions);
|
||||
|
||||
this.scrollableElement.setScrollDimensions({
|
||||
width: typeof width === 'number' ? width : DOM.getContentWidth(this.domNode)
|
||||
});
|
||||
|
||||
this.columnSashs.forEach(s => s.sash.layout());
|
||||
}
|
||||
|
||||
getScrollTop(): number {
|
||||
const scrollPosition = this.scrollableElement.getScrollPosition();
|
||||
return scrollPosition.scrollTop;
|
||||
}
|
||||
|
||||
getScrollLeft(): number {
|
||||
const scrollPosition = this.scrollableElement.getScrollPosition();
|
||||
return scrollPosition.scrollLeft;
|
||||
}
|
||||
|
||||
setScrollTop(scrollTop: number): void {
|
||||
if (this.scrollableElementUpdateDisposable) {
|
||||
this.scrollableElementUpdateDisposable.dispose();
|
||||
this.scrollableElementUpdateDisposable = null;
|
||||
this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });
|
||||
}
|
||||
|
||||
this.scrollableElement.setScrollPosition({ scrollTop });
|
||||
}
|
||||
|
||||
get scrollTop(): number {
|
||||
return this.getScrollTop();
|
||||
}
|
||||
|
||||
set scrollTop(scrollTop: number) {
|
||||
this.setScrollTop(scrollTop);
|
||||
}
|
||||
|
||||
get scrollHeight(): number {
|
||||
return this._scrollHeight + 10;
|
||||
}
|
||||
|
||||
get scrollWidth(): number {
|
||||
return this._scrollWidth + 10;
|
||||
}
|
||||
|
||||
get scrollLeft(): number {
|
||||
return this.getScrollLeft();
|
||||
}
|
||||
|
||||
domElement(index: number, column: number): HTMLElement | null {
|
||||
const row = this.visibleRows[index];
|
||||
const cell = row && row.cells && row.cells[column];
|
||||
return cell && cell.domNode;
|
||||
}
|
||||
|
||||
element(index: number): T | undefined {
|
||||
return this.visibleRows[index].element;
|
||||
}
|
||||
|
||||
column(index: number): ITableColumn<T, any> | undefined {
|
||||
return this.columns[index];
|
||||
}
|
||||
|
||||
indexOfColumn(columnId: string): number | undefined {
|
||||
return firstIndex(this.columns, v => v.id === columnId);
|
||||
}
|
||||
|
||||
get renderHeight(): number {
|
||||
const scrollDimensions = this.scrollableElement.getScrollDimensions();
|
||||
return scrollDimensions.height;
|
||||
}
|
||||
|
||||
private insertRowInDOM(index: number, beforeElement: HTMLElement | null): void {
|
||||
let row = this.visibleRows[index];
|
||||
|
||||
if (!row) {
|
||||
row = {
|
||||
id: String(index),
|
||||
element: undefined,
|
||||
row: null,
|
||||
size: this.rowHeight,
|
||||
cells: null,
|
||||
datapromise: null
|
||||
};
|
||||
row.datapromise = createCancelablePromise(token => {
|
||||
return this.dataSource.getRow(index).then(d => {
|
||||
row.element = d;
|
||||
});
|
||||
});
|
||||
row.datapromise.finally(() => row.datapromise = null);
|
||||
this.visibleRows[index] = row;
|
||||
}
|
||||
|
||||
if (!row.row) {
|
||||
this.allocRow(row, index);
|
||||
row.row!.setAttribute('role', 'treeitem');
|
||||
}
|
||||
|
||||
if (!row.row!.parentElement) {
|
||||
if (beforeElement) {
|
||||
this.rowsContainer.insertBefore(row.row!, beforeElement);
|
||||
} else {
|
||||
this.rowsContainer.appendChild(row.row!);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateRowInDOM(row, index);
|
||||
|
||||
if (row.datapromise) {
|
||||
row.datapromise.then(() => this.renderRow(row, index));
|
||||
// in this case we can special case the row count column
|
||||
for (const [i, column] of this.staticColumns.entries()) {
|
||||
const cell = row.cells![i];
|
||||
column.renderer.renderCell(undefined, index, i, column.id, cell.templateData, column.width);
|
||||
}
|
||||
} else {
|
||||
this.renderRow(row, index);
|
||||
}
|
||||
}
|
||||
|
||||
private allocRow(row: IAsyncRowItem<T>, index: number): void {
|
||||
row.cells = new Array<ICell>();
|
||||
row.row = DOM.$('.monaco-perftable-row');
|
||||
for (const [index, column] of this.columns.entries()) {
|
||||
const cell = this.cache.alloc(column.id);
|
||||
row.cells[index] = cell;
|
||||
if (column.cellClass) {
|
||||
DOM.addClass(cell.domNode!, column.cellClass);
|
||||
}
|
||||
row.row.appendChild(cell.domNode!);
|
||||
}
|
||||
}
|
||||
|
||||
private renderRow(row: IAsyncRowItem<T>, index: number): void {
|
||||
for (const [i, column] of this.columns.entries()) {
|
||||
const cell = row.cells![i];
|
||||
column.renderer.renderCell(row.element!, index, i, column.id, cell.templateData, column.width);
|
||||
}
|
||||
}
|
||||
|
||||
private updateRowInDOM(row: IAsyncRowItem<T>, index: number): void {
|
||||
row.row!.style.top = `${this.elementTop(index)}px`;
|
||||
row.row!.style.height = `${row.size}px`;
|
||||
|
||||
for (const [columnIndex, column] of this.columns.entries()) {
|
||||
const cell = row.cells![columnIndex].domNode;
|
||||
cell!.style.width = `${column.width}px`;
|
||||
cell!.style.left = `${column.left}px`;
|
||||
cell!.style.height = `${row.size}px`;
|
||||
cell!.style.lineHeight = `${row.size}px`;
|
||||
cell!.setAttribute('data-column-id', `${columnIndex}`);
|
||||
cell!.setAttribute('role', 'gridcell');
|
||||
row.row!.setAttribute('id', this.getElementDomId(index, columnIndex));
|
||||
}
|
||||
|
||||
row.row!.setAttribute('data-index', `${index}`);
|
||||
row.row!.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false');
|
||||
row.row!.setAttribute('role', 'row');
|
||||
// row.row!.setAttribute('aria-setsize', String(this.ariaSetProvider.getSetSize(row.element, index, this.length)));
|
||||
// row.row!.setAttribute('aria-posinset', String(this.ariaSetProvider.getPosInSet(row.element, index)));
|
||||
row.row!.setAttribute('id', this.getElementDomId(index));
|
||||
}
|
||||
|
||||
private removeRowFromDOM(index: number): void {
|
||||
const item = this.visibleRows[index];
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
let canceled = false;
|
||||
if (item.datapromise) {
|
||||
item.datapromise.cancel();
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
for (const [i, column] of this.columns.entries()) {
|
||||
const cell = item.cells![i];
|
||||
|
||||
if (!canceled) {
|
||||
const renderer = column.renderer;
|
||||
if (renderer && renderer.disposeCell) {
|
||||
renderer.disposeCell(item.element!, index, i, column.id, cell.templateData, column.width);
|
||||
}
|
||||
}
|
||||
|
||||
this.cache.release(cell!);
|
||||
}
|
||||
|
||||
removeFromParent(item.row!);
|
||||
|
||||
delete this.visibleRows[index];
|
||||
}
|
||||
|
||||
@memoize get onMouseClick(): Event<ITableMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'click'), e => this.toMouseEvent(e)); }
|
||||
@memoize get onMouseDblClick(): Event<ITableMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'dblclick'), e => this.toMouseEvent(e)); }
|
||||
@memoize get onMouseMiddleClick(): Event<ITableMouseEvent<T>> { return Event.filter(Event.map(domEvent(this.domNode, 'auxclick'), e => this.toMouseEvent(e as MouseEvent)), e => e.browserEvent.button === 1); }
|
||||
@memoize get onMouseUp(): Event<ITableMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'mouseup'), e => this.toMouseEvent(e)); }
|
||||
@memoize get onMouseDown(): Event<ITableMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'mousedown'), e => this.toMouseEvent(e)); }
|
||||
@memoize get onMouseOver(): Event<ITableMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'mouseover'), e => this.toMouseEvent(e)); }
|
||||
@memoize get onMouseMove(): Event<ITableMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'mousemove'), e => this.toMouseEvent(e)); }
|
||||
@memoize get onMouseOut(): Event<ITableMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'mouseout'), e => this.toMouseEvent(e)); }
|
||||
@memoize get onContextMenu(): Event<ITableMouseEvent<T>> { return Event.map(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)); }
|
||||
|
||||
public toMouseEvent(browserEvent: MouseEvent): ITableMouseEvent<T> {
|
||||
const index = this.getItemIndexFromEventTarget(browserEvent.target || null);
|
||||
const item = typeof index === 'undefined' ? undefined : this.visibleRows[index.row];
|
||||
const element = item && item.element;
|
||||
const buttons = browserEvent.buttons;
|
||||
return { browserEvent, buttons, index, element };
|
||||
}
|
||||
|
||||
public getItemIndexFromEventTarget(target: EventTarget | null): GridPosition | undefined {
|
||||
let element: HTMLElement | null = target as (HTMLElement | null);
|
||||
|
||||
while (element instanceof HTMLElement && element !== this.rowsContainer) {
|
||||
const rawColumn = element.getAttribute('data-column-id');
|
||||
|
||||
if (rawColumn) {
|
||||
const column = Number(rawColumn);
|
||||
|
||||
if (!isNaN(column)) {
|
||||
while (element instanceof HTMLElement && element !== this.rowsContainer) {
|
||||
const rawIndex = element.getAttribute('data-index');
|
||||
|
||||
if (rawIndex) {
|
||||
const row = Number(rawIndex);
|
||||
|
||||
if (!isNaN(row)) {
|
||||
return new GridPosition(row, column);
|
||||
}
|
||||
}
|
||||
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
element = element!.parentElement;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
elementTop(index: number): number {
|
||||
return Math.floor(index * this.rowHeight) - this.bigNumberDelta;
|
||||
}
|
||||
|
||||
getElementDomId(index: number, column?: number): string {
|
||||
if (column) {
|
||||
return `${this.domId}_${index}_${column}`;
|
||||
} else {
|
||||
return `${this.domId}_${index}`;
|
||||
}
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this._length;
|
||||
}
|
||||
|
||||
set length(length: number) {
|
||||
this.domNode.setAttribute('aria-rowcount', `${length}`);
|
||||
const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
|
||||
const potentialRerenderRange = { start: this.length, end: length };
|
||||
const rerenderRange = Range.intersect(potentialRerenderRange, previousRenderRange);
|
||||
this._length = length;
|
||||
for (let i = rerenderRange.start; i < rerenderRange.end; i++) {
|
||||
if (this.visibleRows[i]) {
|
||||
this.removeRowFromDOM(i);
|
||||
}
|
||||
this.insertRowInDOM(i, null);
|
||||
}
|
||||
this.eventuallyUpdateScrollDimensions();
|
||||
}
|
||||
|
||||
get columnLength(): number {
|
||||
return this.columns.length;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
1060
src/sql/base/browser/ui/table/highPerf/tableWidget.ts
Normal file
1060
src/sql/base/browser/ui/table/highPerf/tableWidget.ts
Normal file
File diff suppressed because it is too large
Load Diff
155
src/sql/base/browser/ui/table/highPerf/virtualizedWindow.ts
Normal file
155
src/sql/base/browser/ui/table/highPerf/virtualizedWindow.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
|
||||
class DataWindow<T> {
|
||||
private _data: T[] | undefined;
|
||||
private _length: number = 0;
|
||||
private _offsetFromDataSource: number = -1;
|
||||
|
||||
private dataReady?: CancelablePromise<void>;
|
||||
|
||||
constructor(
|
||||
private loadFunction: (offset: number, count: number) => Promise<T[]>
|
||||
) { }
|
||||
|
||||
dispose() {
|
||||
this._data = undefined;
|
||||
if (this.dataReady) {
|
||||
this.dataReady.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
get start(): number {
|
||||
return this._offsetFromDataSource;
|
||||
}
|
||||
|
||||
get end(): number {
|
||||
return this._offsetFromDataSource + this._length;
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this._length;
|
||||
}
|
||||
|
||||
public contains(dataSourceIndex: number): boolean {
|
||||
return dataSourceIndex >= this.start && dataSourceIndex < this.end;
|
||||
}
|
||||
|
||||
public getItem(index: number): Promise<T> {
|
||||
return this.dataReady!.then(() => this._data![index - this._offsetFromDataSource]);
|
||||
}
|
||||
|
||||
public positionWindow(offset: number, length: number): void {
|
||||
this._offsetFromDataSource = offset;
|
||||
this._length = length;
|
||||
this._data = undefined;
|
||||
|
||||
if (this.dataReady) {
|
||||
this.dataReady.cancel();
|
||||
}
|
||||
|
||||
if (length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataReady = createCancelablePromise(token => {
|
||||
return this.loadFunction(offset, length).then(data => {
|
||||
if (!token.isCancellationRequested) {
|
||||
this._data = data;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class VirtualizedWindow<T> {
|
||||
private _bufferWindowBefore: DataWindow<T>;
|
||||
private _window: DataWindow<T>;
|
||||
private _bufferWindowAfter: DataWindow<T>;
|
||||
|
||||
constructor(
|
||||
private readonly windowSize: number,
|
||||
private _length: number,
|
||||
loadFn: (offset: number, count: number) => Promise<T[]>
|
||||
) {
|
||||
|
||||
this._bufferWindowBefore = new DataWindow(loadFn);
|
||||
this._window = new DataWindow(loadFn);
|
||||
this._bufferWindowAfter = new DataWindow(loadFn);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._bufferWindowAfter.dispose();
|
||||
this._bufferWindowBefore.dispose();
|
||||
this._window.dispose();
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this._length;
|
||||
}
|
||||
|
||||
set length(length: number) {
|
||||
if (this.length !== length) {
|
||||
const oldLength = this.length;
|
||||
this._length = length;
|
||||
if (this._window.length !== this.windowSize) {
|
||||
this.resetWindowsAroundIndex(oldLength);
|
||||
} else if (this._bufferWindowAfter.length !== this.windowSize) {
|
||||
this._bufferWindowAfter.positionWindow(this._bufferWindowAfter.start, Math.min(this._bufferWindowAfter.start + this.windowSize, this.length));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getIndex(index: number): Promise<T> {
|
||||
|
||||
if (index < this._bufferWindowBefore.start || index >= this._bufferWindowAfter.end) {
|
||||
this.resetWindowsAroundIndex(index);
|
||||
}
|
||||
// scrolling up
|
||||
else if (this._bufferWindowBefore.contains(index)) {
|
||||
const beforeWindow = this._bufferWindowAfter;
|
||||
this._bufferWindowAfter = this._window;
|
||||
this._window = this._bufferWindowBefore;
|
||||
this._bufferWindowBefore = beforeWindow;
|
||||
// ensure we aren't buffer invalid data
|
||||
const beforeStart = Math.max(0, this._window.start - this.windowSize);
|
||||
// ensure if we got hinder in our start index that we update out length to not overlap
|
||||
const beforeLength = this._window.start - beforeStart;
|
||||
this._bufferWindowBefore.positionWindow(beforeStart, beforeLength);
|
||||
}
|
||||
// scroll down
|
||||
else if (this._bufferWindowAfter.contains(index)) {
|
||||
const afterWindow = this._bufferWindowBefore;
|
||||
this._bufferWindowBefore = this._window;
|
||||
this._window = this._bufferWindowAfter;
|
||||
this._bufferWindowAfter = afterWindow;
|
||||
// ensure we aren't buffer invalid data
|
||||
const afterStart = this._window.end;
|
||||
// ensure if we got hinder in our start index that we update out length to not overlap
|
||||
const afterLength = afterStart + this.windowSize > this.length ? this.length - afterStart : this.windowSize;
|
||||
this._bufferWindowAfter.positionWindow(afterStart, afterLength);
|
||||
}
|
||||
|
||||
// at this point we know the current window will have the index
|
||||
return this._window.getItem(index);
|
||||
}
|
||||
|
||||
private resetWindowsAroundIndex(index: number): void {
|
||||
|
||||
let bufferWindowBeforeStart = Math.max(0, index - this.windowSize * 1.5);
|
||||
let bufferWindowBeforeEnd = Math.max(0, index - this.windowSize / 2);
|
||||
this._bufferWindowBefore.positionWindow(bufferWindowBeforeStart, bufferWindowBeforeEnd - bufferWindowBeforeStart);
|
||||
|
||||
let mainWindowStart = bufferWindowBeforeEnd;
|
||||
let mainWindowEnd = Math.min(mainWindowStart + this.windowSize, this.length);
|
||||
this._window.positionWindow(mainWindowStart, mainWindowEnd - mainWindowStart);
|
||||
|
||||
let bufferWindowAfterStart = mainWindowEnd;
|
||||
let bufferWindowAfterEnd = Math.min(bufferWindowAfterStart + this.windowSize, this.length);
|
||||
this._bufferWindowAfter.positionWindow(bufferWindowAfterStart, bufferWindowAfterEnd - bufferWindowAfterStart);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user