mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-20 09:35:38 -05:00
Adding ability to expand and columns slickgrid table rows (#19168)
* Adding ability to expand and columns tables * Bolding icons Fixing variable names * Adding helper function Making css more target * Adding keyboard navigation and parsing treegrid data * Adding attributes, data transformations and key events to the treegrid * Expanded * changing var name * FIxing formatter name * Adding back cell styling * Removing comments * Making a new TreeGrid component. Separating treegrid logic from tableDataView * Fixing comments * changing method name * Modifying only visible row dom attributes * Removing white space, moving role attribute setter to constructor. * Fixing some more PR comments * Adding comments and renaming functions * Fixing comments * Fixing comments * Fixing comments * Fixing some logic and removing unused attributes from element * Adding expandable formatter to the first column * Making the formatter generic * Reverting formatter code * Adding doc comments * Fixing comments * Removing duplicated code * Adding comments * Setting columns only when the table is initialized * Letting users set expanded state instead of forcing it to false
This commit is contained in:
@@ -170,6 +170,27 @@ export function slickGridDataItemColumnValueWithNoData(value: any, columnDef: an
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatter for the first column of the treegrid. The created formatter will wrap the output of the provided formatter with a level based indentation and a chevron icon for tree grid parents that indicates their expand/collapse state.
|
||||
*/
|
||||
export function createTreeGridExpandableColumnFormatter<T>(formattingFunction: Slick.Formatter<T>): Slick.Formatter<T> {
|
||||
return (row: number | undefined, cell: any | undefined, value: any, columnDef: any | undefined, dataContext: any | undefined): string => {
|
||||
const spacer = `<span style='display:inline-block;height:1px;width:${(15 * (dataContext['level'] - 1))}px'></span>`;
|
||||
|
||||
const innerCellContent = formattingFunction(row, cell, value, columnDef, dataContext);
|
||||
|
||||
if (dataContext['isParent']) {
|
||||
if (dataContext.expanded) {
|
||||
return `<div>${spacer}<span class='codicon codicon-chevron-down toggle' style='font-weight:bold;'></span> ${innerCellContent}</div>`;
|
||||
} else {
|
||||
return `<div>${spacer}<span class='codicon codicon-chevron-right toggle' style='font-weight:bold;'></span> ${innerCellContent}</div>`;
|
||||
}
|
||||
} else {
|
||||
return `${spacer}${innerCellContent}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** The following code is a rewrite over the both formatter function using dom builder
|
||||
* rather than string manipulation, which is a safer and easier method of achieving the same goal.
|
||||
* However, when electron is in "Run as node" mode, dom creation acts differently than normal and therefore
|
||||
|
||||
@@ -234,3 +234,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slick-cell .toggle {
|
||||
font-size: 12px !important;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
@@ -33,14 +33,14 @@ export class Table<T extends Slick.SlickData> extends Widget implements IDisposa
|
||||
private styleElement: HTMLStyleElement;
|
||||
private idPrefix: string;
|
||||
|
||||
private _grid: Slick.Grid<T>;
|
||||
private _columns: Slick.Column<T>[];
|
||||
private _data: IDisposableDataProvider<T>;
|
||||
protected _grid: Slick.Grid<T>;
|
||||
protected _columns: Slick.Column<T>[];
|
||||
protected _data: IDisposableDataProvider<T>;
|
||||
private _sorter?: ITableSorter<T>;
|
||||
|
||||
private _autoscroll?: boolean;
|
||||
private _container: HTMLElement;
|
||||
private _tableContainer: HTMLElement;
|
||||
protected _tableContainer: HTMLElement;
|
||||
|
||||
private _classChangeTimeout: any;
|
||||
|
||||
@@ -69,12 +69,6 @@ export class Table<T extends Slick.SlickData> extends Widget implements IDisposa
|
||||
|
||||
this._register(this._data);
|
||||
|
||||
if (configuration && configuration.columns) {
|
||||
this._columns = configuration.columns;
|
||||
} else {
|
||||
this._columns = new Array<Slick.Column<T>>();
|
||||
}
|
||||
|
||||
let newOptions = mixin(options || {}, getDefaultOptions<T>(), false);
|
||||
|
||||
this._container = document.createElement('div');
|
||||
@@ -98,7 +92,14 @@ export class Table<T extends Slick.SlickData> extends Widget implements IDisposa
|
||||
this._tableContainer = document.createElement('div');
|
||||
this._container.appendChild(this._tableContainer);
|
||||
this.styleElement = DOM.createStyleSheet(this._container);
|
||||
this._grid = new Slick.Grid<T>(this._tableContainer, this._data, this._columns, newOptions);
|
||||
this._grid = new Slick.Grid<T>(this._tableContainer, this._data, [], newOptions);
|
||||
|
||||
if (configuration && configuration.columns) {
|
||||
this.columns = configuration.columns;
|
||||
} else {
|
||||
this.columns = new Array<Slick.Column<T>>();
|
||||
}
|
||||
|
||||
this.idPrefix = this._tableContainer.classList[0];
|
||||
this._container.classList.add(this.idPrefix);
|
||||
if (configuration && configuration.sorter) {
|
||||
@@ -175,6 +176,7 @@ export class Table<T extends Slick.SlickData> extends Widget implements IDisposa
|
||||
this._data = new TableDataView<T>(data);
|
||||
}
|
||||
this._grid.setData(this._data, true);
|
||||
this._data.filter(this._grid.getColumns());
|
||||
}
|
||||
|
||||
getData(): IDisposableDataProvider<T> {
|
||||
|
||||
@@ -20,7 +20,7 @@ export type TableFilterFunc<T extends Slick.SlickData> = (data: Array<T>, column
|
||||
export type TableSortFunc<T extends Slick.SlickData> = (args: Slick.OnSortEventArgs<T>, data: Array<T>) => Array<T>;
|
||||
export type TableFindFunc<T extends Slick.SlickData> = (val: T, exp: string) => Array<number>;
|
||||
|
||||
function defaultCellValueGetter(data: any): any {
|
||||
export function defaultCellValueGetter(data: any): any {
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function defaultSort<T extends Slick.SlickData>(args: Slick.OnSortEventAr
|
||||
return data.sort((a, b) => comparer(a, b) * sign);
|
||||
}
|
||||
|
||||
function defaultFilter<T extends Slick.SlickData>(data: T[], columns: FilterableColumn<T>[], cellValueGetter: CellValueGetter = defaultCellValueGetter): T[] {
|
||||
export function defaultFilter<T extends Slick.SlickData>(data: T[], columns: FilterableColumn<T>[], cellValueGetter: CellValueGetter = defaultCellValueGetter): T[] {
|
||||
let filteredData = data;
|
||||
columns?.forEach(column => {
|
||||
if (column.filterValues?.length > 0 && column.field) {
|
||||
|
||||
183
src/sql/base/browser/ui/table/treeGrid.ts
Normal file
183
src/sql/base/browser/ui/table/treeGrid.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./media/slick.grid';
|
||||
|
||||
import { FilterableColumn, ITableConfiguration } from 'sql/base/browser/ui/table/interfaces';
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { IDisposableDataProvider } from 'sql/base/common/dataProvider';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { CellValueGetter, defaultCellValueGetter, defaultFilter, TableDataView } from 'sql/base/browser/ui/table/tableDataView';
|
||||
import { AsyncDataProvider } from 'sql/base/browser/ui/table/asyncDataView';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { createTreeGridExpandableColumnFormatter, textFormatter } from 'sql/base/browser/ui/table/formatters';
|
||||
|
||||
function defaultTreeGridFilter<T extends Slick.SlickData>(data: T[], columns: FilterableColumn<T>[], cellValueGetter: CellValueGetter = defaultCellValueGetter): T[] {
|
||||
let filteredData = defaultFilter(data, columns, cellValueGetter);
|
||||
|
||||
// filtering out rows which have parent/grandparents collapsed.
|
||||
filteredData = filteredData.filter((item) => {
|
||||
let parent = data[item.parent];
|
||||
while (parent) {
|
||||
if (!parent.expanded) {
|
||||
return false;
|
||||
}
|
||||
parent = data[parent.parent];
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
/**
|
||||
* TreeGrid component displays a hierarchical table data grouped into expandable and collapsible nodes.
|
||||
*/
|
||||
export class TreeGrid<T extends Slick.SlickData> extends Table<T> {
|
||||
constructor(parent: HTMLElement, configuration?: ITableConfiguration<T>, options?: Slick.GridOptions<T>) {
|
||||
super(parent, configuration, options);
|
||||
this._tableContainer.setAttribute('role', 'treegrid');
|
||||
if (configuration?.dataProvider && configuration.dataProvider instanceof TableDataView) {
|
||||
this._data = configuration.dataProvider;
|
||||
} else {
|
||||
this._data = new TableDataView<T>(configuration && configuration.dataProvider as Array<T>,
|
||||
undefined,
|
||||
undefined,
|
||||
defaultTreeGridFilter,
|
||||
undefined);
|
||||
}
|
||||
|
||||
this._grid.onClick.subscribe((e, data) => {
|
||||
this.setRowExpandedState(data.row);
|
||||
return false;
|
||||
});
|
||||
|
||||
// The events returned by grid are Jquery events. These events can be handled by returning false which executes preventDefault and stopPropagation
|
||||
this._grid.onKeyDown.subscribe((e, data) => {
|
||||
const keyboardEvent = (<any>e).originalEvent;
|
||||
if (keyboardEvent instanceof KeyboardEvent) {
|
||||
let event = new StandardKeyboardEvent(keyboardEvent);
|
||||
if (event.keyCode === KeyCode.Enter) {
|
||||
// toggle the collapsed state of the row
|
||||
this.setRowExpandedState(data.row);
|
||||
return false;
|
||||
} else if (event.keyCode === KeyCode.LeftArrow) {
|
||||
// Left arrow on first cell of the expanded row collapses it
|
||||
if (data.cell === 0) {
|
||||
this.setRowExpandedState(data.row, false); // Collapsing state
|
||||
return false;
|
||||
}
|
||||
} else if (event.keyCode === KeyCode.RightArrow) {
|
||||
// Right arrow on last cell of the collapsed row expands it.
|
||||
if (data.cell === (this._grid.getColumns().length - 1)) {
|
||||
this.setRowExpandedState(data.row, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
this._grid.onRendered.subscribe((e, data) => {
|
||||
const visibleRows = this._grid.getViewport();
|
||||
for (let i = visibleRows.top; i <= visibleRows.bottom; i++) {
|
||||
const rowData = this._data.getItem(i);
|
||||
// Getting the row div that corresponds to the data row
|
||||
const rowElement = this._tableContainer.querySelector(`div [role="row"][aria-rowindex="${(i + 1)}"]`);
|
||||
// If the row element is found in the dom, we are setting the required aria attributes for it.
|
||||
if (rowElement) {
|
||||
if (rowData.expanded !== undefined) {
|
||||
rowElement.ariaExpanded = rowData.expanded;
|
||||
} else {
|
||||
rowElement.removeAttribute('aria-expanded');
|
||||
}
|
||||
if (rowData.setSize !== undefined) {
|
||||
rowElement.ariaSetSize = rowData.setSize;
|
||||
} else {
|
||||
rowElement.removeAttribute('aria-setsize');
|
||||
}
|
||||
if (rowData.posInSet !== undefined) {
|
||||
rowElement.ariaPosInSet = rowData.posInSet;
|
||||
} else {
|
||||
rowElement.removeAttribute('aria-posinset');
|
||||
}
|
||||
if (rowData.level !== undefined) {
|
||||
rowElement.ariaLevel = rowData.level;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
override setData(data: Array<T>): void;
|
||||
override setData(data: TableDataView<T>): void;
|
||||
override setData(data: AsyncDataProvider<T>): void;
|
||||
override setData(data: Array<T> | TableDataView<T> | AsyncDataProvider<T>): void {
|
||||
if (data instanceof TableDataView || data instanceof AsyncDataProvider) {
|
||||
this._data = data;
|
||||
} else {
|
||||
this._data = new TableDataView<T>(data, undefined, undefined, defaultTreeGridFilter);
|
||||
}
|
||||
this.addTreeGridDataAttributes(this._data);
|
||||
this._grid.setData(this._data, true);
|
||||
this._data.filter(this._grid.getColumns());
|
||||
}
|
||||
|
||||
public override set columns(columns: Slick.Column<T>[]) {
|
||||
if (columns[0]) {
|
||||
// Create a new formatter for the first column that adds level based indentation and a chevron icon.
|
||||
columns[0].formatter = createTreeGridExpandableColumnFormatter(columns[0].formatter ?? textFormatter);
|
||||
}
|
||||
super.columns = columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the expanded state to the specified value, or if undefined toggles the current state of the cell
|
||||
*/
|
||||
private setRowExpandedState(row: number, expanded?: boolean): void {
|
||||
const rowData = this._data.getItem(row);
|
||||
if (rowData['isParent']) {
|
||||
if (expanded === undefined) {
|
||||
(<any>rowData).expanded = !rowData.expanded;
|
||||
} else {
|
||||
(<any>rowData).expanded = expanded;
|
||||
}
|
||||
this._data.filter(this._grid.getColumns());
|
||||
this.rerenderGrid();
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds additional properties to data rows necessary for displaying as part of the tree grid structure.
|
||||
*/
|
||||
private addTreeGridDataAttributes(data: IDisposableDataProvider<T>): void {
|
||||
for (let i = 0; i < data.getLength(); i++) {
|
||||
const dataRow = <any>data.getItem(i);
|
||||
if (dataRow.parent === undefined || dataRow.parent === -1) {
|
||||
dataRow.level = 1;
|
||||
} else {
|
||||
const parentRow = <any>data.getItem(dataRow.parent);
|
||||
dataRow.level = parentRow.level + 1;
|
||||
if (parentRow.setSize === undefined) {
|
||||
parentRow.setSize = 1;
|
||||
} else {
|
||||
parentRow.setSize += 1;
|
||||
}
|
||||
dataRow.posInSet = parentRow.setSize;
|
||||
if (parentRow.expanded === undefined) {
|
||||
parentRow.expanded = false;
|
||||
}
|
||||
parentRow.isParent = true;
|
||||
if (!parentRow._guid) {
|
||||
parentRow._guid = generateUuid();
|
||||
}
|
||||
dataRow.parentGuid = parentRow._guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user