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:
Aasim Khan
2022-05-09 15:53:22 -07:00
committed by GitHub
parent adf6f253f0
commit 7e57503aa6
7 changed files with 233 additions and 31 deletions

View File

@@ -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>&nbsp; ${innerCellContent}</div>`;
} else {
return `<div>${spacer}<span class='codicon codicon-chevron-right toggle' style='font-weight:bold;'></span>&nbsp; ${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

View File

@@ -234,3 +234,8 @@
display: flex;
align-items: center;
}
.slick-cell .toggle {
font-size: 12px !important;
margin-right: -5px;
}

View File

@@ -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> {

View File

@@ -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) {

View 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;
}
}
}
}