Table Filter improvements (#15207)

* use virutualized list

* comments
This commit is contained in:
Alan Ren
2021-04-22 12:03:17 -07:00
committed by GitHub
parent 37894c9e96
commit 228a5d8e20
9 changed files with 346 additions and 278 deletions

View File

@@ -218,10 +218,6 @@ export class AsyncDataProvider<T extends Slick.SlickData> implements IDisposable
throw new Error('Method not implemented.');
}
getFilteredColumnValues(column: Slick.Column<T>): Promise<string[]> {
throw new Error('Method not implemented.');
}
getColumnValues(column: Slick.Column<T>): Promise<string[]> {
throw new Error('Method not implemented.');
}

View File

@@ -68,11 +68,6 @@ export class HybridDataProvider<T extends Slick.SlickData> implements IDisposabl
return this.provider.getColumnValues(column);
}
public async getFilteredColumnValues(column: Slick.Column<T>): Promise<string[]> {
await this.initializeCacheIfNeeded();
return this.provider.getFilteredColumnValues(column);
}
public get dataRows(): IObservableCollection<T> {
return this._asyncDataProvider.dataRows;
}

View File

@@ -88,16 +88,55 @@
padding: 4px;
}
.slick-header-menu .searchbox-row
{
display: flex;
align-items: center;
padding-left: 5px;
margin-top: 5px;
}
.slick-header-menu .searchbox-row .select-all-checkbox
{
flex: 0;
}
.slick-header-menu .searchbox-row .search-input
{
flex: 1 1 auto;
}
.slick-header-menu .searchbox-row .search-input input
{
padding-right: 70px;
}
.slick-header-menu .searchbox-row .selected-count
{
position: absolute;
right: 8px;
align-self: center;
align-items: center;
display: flex;
}
.slick-header-menu .searchbox-row .visible-count
{
position: absolute;
/* visible count badge is not visible but will be read by the screen reader */
left: -10000px;
}
.slick-header-menu .filter
{
border: 1px solid #BFBDBD;
font-size: 8pt;
height: 250px;
margin-top: 6px;
overflow: scroll;
padding: 4px;
overflow: hidden;
padding: 1px;
white-space: nowrap;
width: 200px;
align-content: flex-start;
display: flex;
flex-direction: column;
@@ -106,6 +145,7 @@
.slick-header-menu .filter .filter-option {
display: flex;
align-items: center;
padding-left: 3px;
}
.slick-header-menu .filter-menu-button-container {
@@ -167,19 +207,6 @@
z-index: 999;
}
.vs-dark .slick-header-menu > input.input {
color: #4a4a4a;
}
.hc-black .slick-header-menu > input.input {
color: #000000;
}
.vs-dark .slick-header-menu,
.hc-black .slick-header-menu {
color: #FFFFFF;
}
.slick-icon-cell-content {
background-position: 5px center !important;
background-repeat: no-repeat;

View File

@@ -1,19 +1,25 @@
// Adopted and converted to typescript from https://github.com/danny-sg/slickgrid-spreadsheet-plugins/blob/master/ext.headerfilter.js
// heavily modified
import { IButtonStyles } from 'vs/base/browser/ui/button/button';
import { IButtonOptions, IButtonStyles } from 'vs/base/browser/ui/button/button';
import { localize } from 'vs/nls';
import { Button } from 'sql/base/browser/ui/button/button';
import { FilterableColumn } from 'sql/base/browser/ui/table/interfaces';
import { escape } from 'sql/base/common/strings';
import { addDisposableListener } from 'vs/base/browser/dom';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { addDisposableListener, EventType, EventHelper, $, isAncestor, clearNode, append } from 'vs/base/browser/dom';
import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { withNullAsUndefined } from 'vs/base/common/types';
import { instanceOfIDisposableDataProvider } from 'sql/base/common/dataProvider';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { IInputBoxStyles, InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { trapKeyboardNavigation } from 'sql/base/browser/dom';
import { IListAccessibilityProvider, IListStyles, List } from 'vs/base/browser/ui/list/listWidget';
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Emitter } from 'vs/base/common/event';
import { CountBadge, ICountBadgetyles } from 'vs/base/browser/ui/countBadge/countBadge';
export type HeaderFilterCommands = 'sort-asc' | 'sort-desc';
@@ -23,7 +29,8 @@ export interface CommandEventArgs<T extends Slick.SlickData> {
command: HeaderFilterCommands
}
export interface ITableFilterStyle extends IButtonStyles, IInputBoxStyles { }
export interface ITableFilterStyles extends IButtonStyles, IInputBoxStyles, IListStyles, ICountBadgetyles {
}
const ShowFilterText: string = localize('headerFilter.showFilter', "Show Filter");
@@ -35,16 +42,22 @@ export class HeaderFilter<T extends Slick.SlickData> {
private grid!: Slick.Grid<T>;
private handler = new Slick.EventHandler();
private $menu?: JQuery<HTMLElement>;
private menu?: HTMLElement;
private okButton?: Button;
private clearButton?: Button;
private cancelButton?: Button;
private sortAscButton?: Button;
private sortDescButton?: Button;
private selectAllCheckBox?: Checkbox;
private searchInputBox?: InputBox;
private workingFilters!: Array<string>;
private countBadge?: CountBadge;
private visibleCountBadge?: CountBadge;
private list?: List<TableFilterListElement>;
private listData?: TableFilterListElement[];
private filteredListData?: TableFilterListElement[];
private elementDisposables?: IDisposable[];
private columnDef!: FilterableColumn<T>;
private filterStyles?: ITableFilterStyle;
private filterStyles?: ITableFilterStyles;
private disposableStore = new DisposableStore();
private _enabled: boolean = true;
@@ -70,15 +83,15 @@ export class HeaderFilter<T extends Slick.SlickData> {
}
private handleKeyDown(e: KeyboardEvent): void {
if (this.$menu && (e.key === 'Escape' || e.keyCode === 27)) {
const event = new StandardKeyboardEvent(e);
if (this.menu && event.keyCode === KeyCode.Escape) {
this.hideMenu();
e.preventDefault();
e.stopPropagation();
EventHelper.stop(e, true);
}
}
private handleBodyMouseDown(e: MouseEvent): void {
if (this.$menu && this.$menu[0] !== e.target && !jQuery.contains(this.$menu[0], e.target as Element)) {
if (this.menu && this.menu !== e.target && !isAncestor(e.target as Element, this.menu)) {
this.hideMenu();
}
}
@@ -92,9 +105,6 @@ export class HeaderFilter<T extends Slick.SlickData> {
return;
}
const column = args.column as FilterableColumn<T>;
if (column.id === '_detail_selector') {
return;
}
if ((<FilterableColumn<T>>column).filterable === false) {
return;
}
@@ -122,69 +132,167 @@ export class HeaderFilter<T extends Slick.SlickData> {
.remove();
}
private createButtonMenuItem(menu: HTMLElement, columnDef: Slick.Column<T>, title: string, command: HeaderFilterCommands, iconClass: string): Button {
const buttonContainer = document.createElement('div');
menu.appendChild(buttonContainer);
private createButtonMenuItem(title: string, command: HeaderFilterCommands, iconClass: string): Button {
const buttonContainer = append(this.menu, $(''));
const button = new Button(buttonContainer);
button.icon = { classNames: `slick-header-menuicon ${iconClass}` };
button.label = title;
button.onDidClick(async () => {
await this.handleMenuItemClick(command, columnDef);
await this.handleMenuItemClick(command, this.columnDef);
});
return button;
}
private createSearchInput(menu: JQuery<HTMLElement>, columnDef: Slick.Column<T>): InputBox {
const inputContainer = document.createElement('div');
inputContainer.style.width = '206px';
inputContainer.style.marginTop = '5px';
menu[0].appendChild(inputContainer);
const input = new InputBox(inputContainer, this.contextViewProvider, {
private createSearchInputRow(): void {
const searchRow = append(this.menu, $('.searchbox-row'));
this.selectAllCheckBox = new Checkbox(append(searchRow, $('.select-all-checkbox')), {
onChange: (val) => {
this.filteredListData.forEach(item => {
item.checked = val;
});
},
label: '',
ariaLabel: localize('table.selectAll', "Select All")
});
this.searchInputBox = new InputBox(append(searchRow, $('.search-input')), this.contextViewProvider, {
placeholder: localize('table.searchPlaceHolder', "Search")
});
input.onDidChange(async (newString) => {
const filterVals = await this.getFilterValuesByInput(columnDef, newString);
this.updateFilterInputs(menu, columnDef, filterVals);
const visibleCountContainer = append(searchRow, $('.visible-count'));
visibleCountContainer.setAttribute('aria-live', 'polite');
visibleCountContainer.setAttribute('aria-atomic', 'true');
this.visibleCountBadge = new CountBadge(visibleCountContainer, {
countFormat: localize({ key: 'tableFilter.visibleCount', comment: ['This tells the user how many items are shown in the list. Currently not visible, but read by screen readers.'] }, "{0} Results")
});
const selectedCountBadgeContainer = append(searchRow, $('.selected-count'));
selectedCountBadgeContainer.setAttribute('aria-live', 'polite');
this.countBadge = new CountBadge(selectedCountBadgeContainer, {
countFormat: localize({ key: 'tableFilter.selectedCount', comment: ['This tells the user how many items are selected in the list'] }, "{0} Selected")
});
this.searchInputBox.onDidChange(async (newString) => {
this.filteredListData = this.listData.filter(element => element.value?.toUpperCase().indexOf(newString.toUpperCase()) !== -1);
this.list.splice(0, this.list.length, this.filteredListData);
this.updateSelectionState();
});
return input;
}
private updateFilterInputs(menu: JQuery<HTMLElement>, columnDef: FilterableColumn<T>, filterItems: Array<string>) {
let filterOptions = '<label><input type="checkbox" value="-1" />(Select All)</label>';
columnDef.filterValues = columnDef.filterValues || [];
private async createFilterList(): Promise<void> {
this.columnDef.filterValues = this.columnDef.filterValues || [];
// WorkingFilters is a copy of the filters to enable apply/cancel behaviour
this.workingFilters = columnDef.filterValues.slice(0);
for (let i = 0; i < filterItems.length; i++) {
const filtered = this.workingFilters.some(x => x === filterItems[i]);
filterOptions += '<label><input type="checkbox" value="' + i + '"'
+ (filtered ? ' checked="checked"' : '')
+ '/>' + filterItems[i] + '</label>';
const workingFilters = this.columnDef.filterValues.slice(0);
let filterItems: Array<string>;
const dataView = this.grid.getData() as Slick.DataProvider<T>;
if (instanceOfIDisposableDataProvider(dataView)) {
filterItems = await dataView.getColumnValues(this.columnDef);
} else {
const filterApplied = this.grid.getColumns().findIndex((col) => {
const filterableColumn = col as FilterableColumn<T>;
return filterableColumn.filterValues?.length > 0;
}) !== -1;
if (!filterApplied) {
// Filter based all available values
filterItems = this.getFilterValues(this.grid.getData() as Slick.DataProvider<T>, this.columnDef);
}
else {
// Filter based on current dataView subset
filterItems = this.getAllFilterValues((this.grid.getData() as Slick.Data.DataView<T>).getFilteredItems(), this.columnDef);
}
}
const $filter = menu.find('.filter');
$filter.empty().append(jQuery(filterOptions));
jQuery(':checkbox', $filter).bind('click', (e) => {
this.workingFilters = this.changeWorkingFilter(filterItems, this.workingFilters, jQuery(e.target));
this.listData = [];
for (let i = 0; i < filterItems.length; i++) {
const filtered = workingFilters.some(x => x === filterItems[i]);
// work item to remove the 'Error:' string check: https://github.com/microsoft/azuredatastudio/issues/15206
if (filterItems[i] && filterItems[i].indexOf('Error:') < 0) {
this.listData.push(new TableFilterListElement(filterItems[i], filtered));
}
}
this.elementDisposables = this.listData.map(element => {
return element.onCheckStateChanged((e) => {
this.updateSelectionState();
});
});
this.filteredListData = this.listData;
const filter = append(this.menu, $('.filter'));
this.list = new List<TableFilterListElement>('TableFilter', filter, new TableFilterListDelegate(), [new TableFilterListRenderer()], {
multipleSelectionSupport: false,
keyboardSupport: true,
mouseSupport: true,
accessibilityProvider: new TableFilterListAccessibilityProvider()
});
this.list.onKeyDown((e) => {
const event = new StandardKeyboardEvent(e);
switch (event.keyCode) {
case KeyCode.Home:
if (this.filteredListData.length > 0) {
this.list.focusFirst();
this.list.reveal(0);
EventHelper.stop(e, true);
}
break;
case KeyCode.End:
if (this.filteredListData.length > 0) {
this.list.focusLast();
this.list.reveal(this.filteredListData.length - 1);
EventHelper.stop(e, true);
}
break;
case KeyCode.Space:
if (this.list.getFocus().length !== 0) {
this.list.setSelection(this.list.getFocus());
this.toggleCheckbox();
EventHelper.stop(e, true);
}
break;
}
});
this.list.splice(0, this.filteredListData.length, this.filteredListData);
this.updateSelectionState();
}
private createButton(container: HTMLElement, id: string, text: string, options?: IButtonOptions): Button {
const buttonContainer = append(container, $('.filter-menu-button'));
const button = new Button(buttonContainer, options);
button.label = button.title = text;
button.element.id = id;
return button;
}
private toggleCheckbox(): void {
if (this.list.getFocus().length !== 0) {
const element = this.list.getFocusedElements()[0];
element.checked = !element.checked;
this.updateSelectionState();
}
}
private updateSelectionState() {
const checkedElements = this.filteredListData.filter(element => element.checked);
this.selectAllCheckBox.checked = this.filteredListData.length > 0 && checkedElements.length === this.filteredListData.length;
this.countBadge.setCount(checkedElements.length);
this.visibleCountBadge.setCount(this.filteredListData.length);
}
private async showFilter(filterButton: HTMLElement): Promise<void> {
await this.createFilterMenu(filterButton);
const menuElement = this.$menu[0];
// Get the absolute coordinates of the filter button
const offset = jQuery(filterButton).offset();
// Calculate the position of menu item
let menuleft = offset.left - menuElement.offsetWidth + filterButton.offsetWidth;
let menuleft = offset.left - this.menu.offsetWidth + filterButton.offsetWidth;
let menutop = offset.top + filterButton.offsetHeight;
// Make sure the entire menu is on screen.
// If there is not enough vertical space under the filter button, we will move up the menu.
// If the left of the menu is off screen (negative value), we will show the menu next to the left edge of window.
// We don't really consider the case when there is not enough space to show the entire menu since in that case the application is not usable already.
if (menutop + menuElement.offsetHeight > window.innerHeight) {
menutop = window.innerHeight - menuElement.offsetHeight;
if (menutop + this.menu.offsetHeight > window.innerHeight) {
menutop = window.innerHeight - this.menu.offsetHeight;
}
menuleft = menuleft > 0 ? menuleft : 0;
@@ -196,13 +304,10 @@ export class HeaderFilter<T extends Slick.SlickData> {
};
},
render: (container: HTMLElement) => {
container.appendChild(menuElement);
container.appendChild(this.menu);
return {
dispose: () => {
if (this.$menu) {
this.$menu.remove();
this.$menu = undefined;
}
this.disposeMenu();
}
};
},
@@ -212,106 +317,56 @@ export class HeaderFilter<T extends Slick.SlickData> {
});
}
private disposeMenu(): void {
if (this.menu) {
clearNode(this.menu);
this.menu.remove();
this.menu = undefined;
dispose(this.elementDisposables);
}
}
private async createFilterMenu(filterButton: HTMLElement) {
const target = withNullAsUndefined(filterButton);
const $menuButton = jQuery(target);
this.columnDef = $menuButton.data('column');
this.columnDef.filterValues = this.columnDef.filterValues || [];
this.disposeMenu();
// WorkingFilters is a copy of the filters to enable apply/cancel behaviour
this.workingFilters = this.columnDef.filterValues.slice(0);
// first add it to the document so that we can get the actual size of the menu
// later, it will be added to the correct container
this.menu = append(document.body, $('.slick-header-menu'));
let filterItems: Array<string>;
this.sortAscButton = this.createButtonMenuItem(localize('table.sortAscending', "Sort Ascending"), 'sort-asc', 'ascending');
this.sortDescButton = this.createButtonMenuItem(localize('table.sortDescending', "Sort Descending"), 'sort-desc', 'descending');
this.createSearchInputRow();
await this.createFilterList();
const dataView = this.grid.getData() as Slick.DataProvider<T>;
if (instanceOfIDisposableDataProvider(dataView)) {
if (this.workingFilters.length === 0) {
filterItems = await dataView.getColumnValues(this.columnDef);
} else {
filterItems = await dataView.getFilteredColumnValues(this.columnDef);
}
} else {
if (this.workingFilters.length === 0) {
// Filter based all available values
filterItems = this.getFilterValues(this.grid.getData() as Slick.DataProvider<T>, this.columnDef);
}
else {
// Filter based on current dataView subset
filterItems = this.getAllFilterValues((this.grid.getData() as Slick.DataProvider<T>).getItems(), this.columnDef);
}
}
if (!this.$menu) {
// first add it to the document so that we can get the actual size of the menu
// later, it will be added to the correct container
this.$menu = jQuery('<div class="slick-header-menu">').appendTo(document.body);
}
this.$menu.empty();
this.sortAscButton = this.createButtonMenuItem(this.$menu[0], this.columnDef, localize('table.sortAscending', "Sort Ascending"), 'sort-asc', 'ascending');
this.sortDescButton = this.createButtonMenuItem(this.$menu[0], this.columnDef, localize('table.sortDescending', "Sort Descending"), 'sort-desc', 'descending');
this.searchInputBox = this.createSearchInput(this.$menu, this.columnDef);
let filterOptions = '<label class="filter-option"><input type="checkbox" value="-1" />(Select All)</label>';
for (let i = 0; i < filterItems.length; i++) {
const filtered = this.workingFilters.some(x => x === filterItems[i]);
if (filterItems[i] && filterItems[i].indexOf('Error:') < 0) {
filterOptions += '<label class="filter-option"><input type="checkbox" value="' + i + '"'
+ (filtered ? ' checked="checked"' : '')
+ '/>' + escape(filterItems[i]) + '</label>';
}
}
const $filter = jQuery('<div class="filter">')
.append(jQuery(filterOptions))
.appendTo(this.$menu);
const $buttonContainer = jQuery('<div class="filter-menu-button-container">').appendTo(this.$menu);
const $okButtonDiv = jQuery('<div class="filter-menu-button">').appendTo($buttonContainer);
const $clearButtonDiv = jQuery('<div class="filter-menu-button">').appendTo($buttonContainer);
const $cancelButtonDiv = jQuery('<div class="filter-menu-button">').appendTo($buttonContainer);
this.okButton = new Button($okButtonDiv.get(0));
this.okButton.label = localize('headerFilter.ok', "OK");
this.okButton.title = localize('headerFilter.ok', "OK");
this.okButton.element.id = 'filter-ok-button';
const buttonGroupContainer = append(this.menu, $('.filter-menu-button-container'));
this.okButton = this.createButton(buttonGroupContainer, 'filter-ok-button', localize('headerFilter.ok', "OK"));
this.okButton.onDidClick(() => {
this.columnDef.filterValues = this.workingFilters.splice(0);
this.columnDef.filterValues = this.listData.filter(element => element.checked).map(element => element.value);
this.setButtonImage($menuButton, this.columnDef.filterValues.length > 0);
this.handleApply(this.columnDef);
});
this.clearButton = new Button($clearButtonDiv.get(0), { secondary: true });
this.clearButton.label = localize('headerFilter.clear', "Clear");
this.clearButton.title = localize('headerFilter.clear', "Clear");
this.clearButton.element.id = 'filter-clear-button';
this.clearButton = this.createButton(buttonGroupContainer, 'filter-clear-button', localize('headerFilter.clear', "Clear"), { secondary: true });
this.clearButton.onDidClick(() => {
this.columnDef.filterValues!.length = 0;
this.setButtonImage($menuButton, false);
this.handleApply(this.columnDef);
});
this.cancelButton = new Button($cancelButtonDiv.get(0), { secondary: true });
this.cancelButton.label = localize('headerFilter.cancel', "Cancel");
this.cancelButton.title = localize('headerFilter.cancel', "Cancel");
this.cancelButton.element.id = 'filter-cancel-button';
this.cancelButton = this.createButton(buttonGroupContainer, 'filter-cancel-button', localize('headerFilter.cancel', "Cancel"), { secondary: true });
this.cancelButton.onDidClick(() => {
this.hideMenu();
});
this.applyStyles();
jQuery(':checkbox', $filter).bind('click', (e) => {
this.workingFilters = this.changeWorkingFilter(filterItems, this.workingFilters, jQuery(e.target));
});
// No need to add this to disposable store, it will be disposed when the menu is closed.
trapKeyboardNavigation(this.$menu[0]);
trapKeyboardNavigation(this.menu);
}
public style(styles: ITableFilterStyle): void {
public style(styles: ITableFilterStyles): void {
this.filterStyles = styles;
this.applyStyles();
}
@@ -324,6 +379,9 @@ export class HeaderFilter<T extends Slick.SlickData> {
this.sortAscButton?.style(this.filterStyles);
this.sortDescButton?.style(this.filterStyles);
this.searchInputBox?.style(this.filterStyles);
this.countBadge?.style(this.filterStyles);
this.visibleCountBadge?.style(this.filterStyles);
this.list?.style(this.filterStyles);
}
}
@@ -331,39 +389,6 @@ export class HeaderFilter<T extends Slick.SlickData> {
this.hideMenu();
}
private changeWorkingFilter(filterItems: Array<string>, workingFilters: Array<string>, $checkbox: JQuery<HTMLElement>) {
const value = $checkbox.val() as number;
const $filter = $checkbox.parent().parent();
if ($checkbox.val() as number < 0) {
// Select All
if ($checkbox.prop('checked')) {
jQuery(':checkbox', $filter).prop('checked', true);
workingFilters = filterItems.slice(0);
} else {
jQuery(':checkbox', $filter).prop('checked', false);
workingFilters.length = 0;
}
} else {
const index = workingFilters.indexOf(filterItems[value]);
if ($checkbox.prop('checked') && index < 0) {
workingFilters.push(filterItems[value]);
const nextRow = filterItems[Number((parseInt(<string><any>value) + 1).toString())]; // for some reason parseInt is defined as only supporting strings even though it works fine for numbers
if (nextRow && nextRow.indexOf('Error:') >= 0) {
workingFilters.push(nextRow);
}
}
else {
if (index > -1) {
workingFilters.splice(index, 1);
}
}
}
return workingFilters;
}
private setButtonImage($el: JQuery<HTMLElement>, filtered: boolean) {
const element: HTMLElement = $el.get(0);
if (filtered) {
@@ -399,34 +424,6 @@ export class HeaderFilter<T extends Slick.SlickData> {
return Array.from(seen);
}
private async getFilterValuesByInput(column: Slick.Column<T>, filter: string): Promise<Array<string>> {
const dataView = this.grid.getData() as Slick.DataProvider<T>,
seen: Set<any> = new Set();
let columnValues: any[];
if (instanceOfIDisposableDataProvider(dataView)) {
columnValues = await dataView.getColumnValues(this.columnDef);
} else {
columnValues = dataView.getItems().map(item => item[column.field]);
}
columnValues.forEach(value => {
const valueArr = value instanceof Array ? value : [(!value ? '' : value)];
if (filter.length > 0) {
const lowercaseFilter = filter.toString().toLowerCase();
valueArr.map(v => v.toLowerCase()).forEach((lowerVal, index) => {
if (lowerVal.indexOf(lowercaseFilter) > -1) {
seen.add(valueArr[index]);
}
});
} else {
valueArr.forEach(v => seen.add(v));
}
});
return Array.from(seen).sort((v) => { return v; });
}
private getAllFilterValues(data: Array<T>, column: Slick.Column<T>) {
const seen: Set<any> = new Set();
@@ -475,3 +472,111 @@ export class HeaderFilter<T extends Slick.SlickData> {
}
}
}
class TableFilterListElement {
private readonly _onCheckStateChanged = new Emitter<boolean>();
private _checked: boolean;
constructor(val: string, checked: boolean) {
this.value = val;
this._checked = checked;
}
public value: string;
public onCheckStateChanged = this._onCheckStateChanged.event;
public get checked(): boolean {
return this._checked;
}
public set checked(val: boolean) {
if (this._checked !== val) {
this._checked = val;
this._onCheckStateChanged.fire(val);
}
}
}
const TableFilterTemplateId = 'TableFilterListTemplate';
class TableFilterListDelegate implements IListVirtualDelegate<TableFilterListElement> {
getHeight(element: TableFilterListElement): number {
return 22;
}
getTemplateId(element: TableFilterListElement): string {
return TableFilterTemplateId;
}
}
interface TableFilterListItemTemplate {
checkbox: HTMLInputElement;
text: HTMLDivElement;
label: HTMLLabelElement;
element: TableFilterListElement;
elementDisposables: IDisposable[];
templateDisposables: IDisposable[];
}
class TableFilterListRenderer implements IListRenderer<TableFilterListElement, TableFilterListItemTemplate> {
renderTemplate(container: HTMLElement): TableFilterListItemTemplate {
const data: TableFilterListItemTemplate = Object.create(null);
data.templateDisposables = [];
data.elementDisposables = [];
data.label = append(container, $<HTMLLabelElement>('label.filter-option'));
data.checkbox = append(data.label, $<HTMLInputElement>('input', {
'type': 'checkbox',
'tabIndex': -1
}));
data.text = append(data.label, $<HTMLDivElement>('div'));
data.text.style.flex = '1 1 auto';
data.templateDisposables.push(addDisposableListener(data.checkbox, EventType.CHANGE, (event) => {
data.element.checked = data.checkbox.checked;
}));
return data;
}
renderElement(element: TableFilterListElement, index: number, templateData: TableFilterListItemTemplate, height: number): void {
templateData.element = element;
templateData.elementDisposables = dispose(templateData.elementDisposables);
templateData.elementDisposables.push(templateData.element.onCheckStateChanged((e) => {
templateData.checkbox.checked = e;
}));
templateData.checkbox.checked = element.checked;
templateData.checkbox.setAttribute('aria-label', element.value);
templateData.text.innerText = element.value;
templateData.label.title = element.value;
}
disposeElement?(element: TableFilterListElement, index: number, templateData: TableFilterListItemTemplate, height: number): void {
templateData.elementDisposables = dispose(templateData.elementDisposables);
}
public disposeTemplate(templateData: TableFilterListItemTemplate): void {
templateData.elementDisposables = dispose(templateData.elementDisposables);
templateData.templateDisposables = dispose(templateData.templateDisposables);
}
public get templateId(): string {
return TableFilterTemplateId;
}
}
class TableFilterListAccessibilityProvider implements IListAccessibilityProvider<TableFilterListElement> {
getAriaLabel(element: TableFilterListElement): string {
return element.value;
}
getWidgetAriaLabel(): string {
return localize('table.filterOptions', "Filter Options");
}
getWidgetRole() {
return 'listbox';
}
getRole(element: TableFilterListElement): string {
return 'option';
}
}

View File

@@ -1,5 +1,6 @@
// Adopted and converted to typescript from https://github.com/6pac/SlickGrid/blob/master/plugins/slick.rowdetailview.js
// heavily modified
import { FilterableColumn } from 'sql/base/browser/ui/table/interfaces';
import { escape } from 'sql/base/common/strings';
import { mixin } from 'vs/base/common/objects';
import * as nls from 'vs/nls';
@@ -338,9 +339,10 @@ export class RowDetailView<T extends Slick.SlickData> {
}
}
public getColumnDefinition(): Slick.Column<T> {
public getColumnDefinition(): FilterableColumn<T> {
return {
id: this._options.columnId,
filterable: false,
name: '',
toolTip: this._options.toolTip,
field: 'sel',

View File

@@ -113,17 +113,9 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
return this._data.slice(startIndex, startIndex + length);
}
public async getFilteredColumnValues(column: Slick.Column<T>): Promise<string[]> {
return this.getDistinctColumnValues(this._data, column);
}
public async getColumnValues(column: Slick.Column<T>): Promise<string[]> {
return this.getDistinctColumnValues(this.filterEnabled ? this._allData : this._data, column);
}
private getDistinctColumnValues(source: T[], column: Slick.Column<T>): string[] {
const distinctValues: Set<string> = new Set();
source.forEach(items => {
this._data.forEach(items => {
const value = items[column.field!];
const valueArr = value instanceof Array ? value : [value];
valueArr.forEach(v => distinctValues.add(this._cellValueGetter(v)));

View File

@@ -26,12 +26,6 @@ export interface IDisposableDataProvider<T> extends Slick.DataProvider<T> {
*/
getColumnValues(column: Slick.Column<T>): Promise<string[]>;
/**
* Gets the unique values of the filtered cells in the given column
* @param column
*/
getFilteredColumnValues(column: Slick.Column<T>): Promise<string[]>;
/**
* Filters the data
* @param columns columns to be filtered, the

View File

@@ -8,7 +8,7 @@ import * as colors from './colors';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import * as cr from 'vs/platform/theme/common/colorRegistry';
import * as sqlcr from 'sql/platform/theme/common/colorRegistry';
import { attachStyler, IColorMapping, IStyleOverrides } from 'vs/platform/theme/common/styler';
import { attachStyler, defaultListStyles, IColorMapping, IStyleOverrides } from 'vs/platform/theme/common/styler';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IThemable } from 'vs/base/common/styler';
@@ -342,6 +342,10 @@ export function attachTableFilterStyler(widget: IThemable, themeService: IThemeS
buttonSecondaryBorder: cr.buttonSecondaryBorder,
buttonDisabledBorder: cr.buttonDisabledBorder,
buttonDisabledBackground: cr.buttonDisabledBackground,
buttonDisabledForeground: cr.buttonDisabledForeground
buttonDisabledForeground: cr.buttonDisabledForeground,
badgeBackground: cr.badgeBackground,
badgeForeground: cr.badgeForeground,
badgeBorder: cr.contrastBorder,
...defaultListStyles,
}, widget);
}

View File

@@ -335,7 +335,6 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
// we need to be able to show distinct array values in filter dialog for columns with array data
filterPlugin['getFilterValues'] = this.getFilterValues;
filterPlugin['getAllFilterValues'] = this.getAllFilterValues;
filterPlugin['getFilterValuesByInput'] = this.getFilterValuesByInput;
dom.clearNode(this._gridEl.nativeElement);
dom.clearNode(this.actionBarContainer.nativeElement);
@@ -588,52 +587,6 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
return seen.sort((v) => { return v; });
}
private async getFilterValuesByInput(column: Slick.Column<any>, filter: string): Promise<Array<string>> {
const dataView = this['grid'].getData() as Slick.DataProvider<Slick.SlickData>,
seen: Array<any> = [];
for (let i = 0; i < dataView.getLength(); i++) {
const value = dataView.getItem(i)[column.field];
if (value instanceof Array) {
if (filter.length > 0) {
const itemValue = !value ? [] : value;
const lowercaseFilter = filter.toString().toLowerCase();
const lowercaseVals = itemValue.map(v => v.toLowerCase());
for (let valIdx = 0; valIdx < value.length; valIdx++) {
if (!seen.some(x => x === value[valIdx]) && lowercaseVals[valIdx].indexOf(lowercaseFilter) > -1) {
seen.push(value[valIdx]);
}
}
}
else {
for (let item = 0; item < value.length; item++) {
if (!seen.some(x => x === value[item])) {
seen.push(value[item]);
}
}
}
} else {
if (filter.length > 0) {
const itemValue = !value ? '' : value;
const lowercaseFilter = filter.toString().toLowerCase();
const lowercaseVal = itemValue.toString().toLowerCase();
if (!seen.some(x => x === value) && lowercaseVal.indexOf(lowercaseFilter) > -1) {
seen.push(value);
}
}
else {
if (!seen.some(x => x === value)) {
seen.push(value);
}
}
}
}
return seen.sort((v) => { return v; });
}
private _updateStyles(theme: IColorTheme): void {
this.actionBarContainer.nativeElement.style.borderTopColor = theme.getColor(themeColors.DASHBOARD_BORDER, true).toString();
let tableStyle: ITableStyles = {