mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-16 01:25:36 -05:00
727 lines
24 KiB
TypeScript
727 lines
24 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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 { 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');
|
|
|
|
this.domNode.classList.add(this.domId);
|
|
this.domNode.tabIndex = 0;
|
|
|
|
this.domNode.classList.toggle('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 = this.columnSashs.findIndex(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 this.columns.findIndex(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) {
|
|
cell.domNode!.classList.add(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);
|
|
}
|
|
}
|