|
|
|
|
@@ -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';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|