Skip to content

Commit

Permalink
Filter button in search box
Browse files Browse the repository at this point in the history
Signed-off-by: Colin Grant <colin.grant@ericsson.com>
  • Loading branch information
Colin Grant committed Oct 7, 2020
1 parent 137840a commit e00d070
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 11 deletions.
17 changes: 16 additions & 1 deletion packages/core/src/browser/style/search-box.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,28 @@
color: var(--theia-editorWidget-foreground);
}

.theia-search-button.codicon.codicon-filter {
line-height: var(--theia-private-horizontal-tab-height);
color: var(--theia-editorWidget-foreground);
align-self: flex-end;
}

.theia-search-button.codicon-filter:not(.filter-active):before {
content: "\eb85";
}

.theia-search-button.codicon-filter.filter-active:before {
content: "\eb83";
}

.theia-search-button-next:before {
content: "\f107";
}

.theia-search-button-next:hover,
.theia-search-button-previous:hover,
.theia-search-button-close:hover {
.theia-search-button-close:hover,
.theia-search-button.codicon-filter:hover {
cursor: pointer;
background-color: var(--theia-editorHoverWidget-background);
}
Expand Down
56 changes: 55 additions & 1 deletion packages/core/src/browser/tree/search-box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export interface SearchBoxProps extends SearchBoxDebounceOptions {
*/
readonly showButtons?: boolean;

/**
* If `true`, `Filter` and `Close` buttons will be visible, and clicking the `Filter` button will triggers filter on the search term. Defaults to `false`.
*/
readonly showFilter?: boolean;

}

export namespace SearchBoxProps {
Expand All @@ -54,7 +59,10 @@ export class SearchBox extends BaseWidget {
protected readonly previousEmitter = new Emitter<void>();
protected readonly closeEmitter = new Emitter<void>();
protected readonly textChangeEmitter = new Emitter<string | undefined>();
protected readonly filterToggleEmitter = new Emitter<boolean>();
protected readonly input: HTMLInputElement;
protected readonly filter: HTMLElement | undefined;
protected _isFiltering: boolean = false;

constructor(protected readonly props: SearchBoxProps,
protected readonly debounce: SearchBoxDebounce) {
Expand All @@ -65,13 +73,15 @@ export class SearchBox extends BaseWidget {
this.previousEmitter,
this.closeEmitter,
this.textChangeEmitter,
this.filterToggleEmitter,
this.debounce,
this.debounce.onChanged(data => this.fireTextChange(data))
]);
this.hide();
this.update();
const { input } = this.createContent();
const { input, filter } = this.createContent();
this.input = input;
this.filter = filter;
}

get onPrevious(): Event<void> {
Expand All @@ -90,6 +100,14 @@ export class SearchBox extends BaseWidget {
return this.textChangeEmitter.event;
}

get onFilterToggled(): Event<boolean> {
return this.filterToggleEmitter.event;
}

get isFiltering(): boolean {
return this._isFiltering;
}

get keyCodePredicate(): KeyCode.Predicate {
return this.canHandle.bind(this);
}
Expand All @@ -110,6 +128,23 @@ export class SearchBox extends BaseWidget {
this.textChangeEmitter.fire(input);
}

protected fireFilterToggle(): void {
this.doFireFilterToggle();
}

protected doFireFilterToggle(toggleTo: boolean = !this._isFiltering): void {
if (this.filter) {
if (toggleTo) {
this.filter.classList.add(SearchBox.Styles.FILTER_ON);
} else {
this.filter.classList.remove(SearchBox.Styles.FILTER_ON);
}
this._isFiltering = toggleTo;
this.filterToggleEmitter.fire(toggleTo);
this.update();
}
}

handle(event: KeyboardEvent): void {
event.preventDefault();
const keyCode = KeyCode.createKeyCode(event);
Expand All @@ -132,6 +167,7 @@ export class SearchBox extends BaseWidget {
}

onBeforeHide(): void {
this.doFireFilterToggle(false);
this.debounce.append(undefined);
this.fireClose();
}
Expand Down Expand Up @@ -164,6 +200,7 @@ export class SearchBox extends BaseWidget {
protected createContent(): {
container: HTMLElement,
input: HTMLInputElement,
filter: HTMLElement | undefined,
previous: HTMLElement | undefined,
next: HTMLElement | undefined,
close: HTMLElement | undefined
Expand All @@ -181,6 +218,18 @@ export class SearchBox extends BaseWidget {
);
this.node.appendChild(input);

let filter: HTMLElement | undefined;
if (this.props.showFilter) {
filter = document.createElement('div');
filter.classList.add(
SearchBox.Styles.BUTTON,
...SearchBox.Styles.FILTER,
);
filter.title = 'Enable Filter on Type';
this.node.appendChild(filter);
filter.onclick = this.fireFilterToggle.bind(this);
}

let previous: HTMLElement | undefined;
let next: HTMLElement | undefined;
let close: HTMLElement | undefined;
Expand All @@ -203,7 +252,9 @@ export class SearchBox extends BaseWidget {
next.title = 'Next (Down)';
this.node.appendChild(next);
next.onclick = () => this.fireNext.bind(this)();
}

if (this.props.showButtons || this.props.showFilter) {
close = document.createElement('div');
close.classList.add(
SearchBox.Styles.BUTTON,
Expand All @@ -217,6 +268,7 @@ export class SearchBox extends BaseWidget {
return {
container: this.node,
input,
filter,
previous,
next,
close
Expand All @@ -242,6 +294,8 @@ export namespace SearchBox {
export const SEARCH_BOX = 'theia-search-box';
export const SEARCH_INPUT = 'theia-search-input';
export const BUTTON = 'theia-search-button';
export const FILTER = ['codicon', 'codicon-filter'];
export const FILTER_ON = 'filter-active';
export const BUTTON_PREVIOUS = 'theia-search-button-previous';
export const BUTTON_NEXT = 'theia-search-button-next';
export const BUTTON_CLOSE = 'theia-search-button-close';
Expand Down
24 changes: 20 additions & 4 deletions packages/core/src/browser/tree/tree-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class TreeSearch implements Disposable {

protected _filterResult: FuzzySearch.Match<TreeNode>[] = [];
protected _filteredNodes: ReadonlyArray<Readonly<TreeNode>> = [];
protected _filteredNodesAndParents: Set<string> = new Set();

@postConstruct()
protected init(): void {
Expand All @@ -55,22 +56,34 @@ export class TreeSearch implements Disposable {
*/
async filter(pattern: string | undefined): Promise<ReadonlyArray<Readonly<TreeNode>>> {
const { root } = this.tree;
this._filteredNodesAndParents = new Set();
if (!pattern || !root) {
this._filterResult = [];
this._filteredNodes = [];
this.fireFilteredNodesChanged(this._filteredNodes);
return [];
}
const items = [...new TopDownTreeIterator(root, { pruneCollapsed: true })];
const items = [...new TopDownTreeIterator(root)];
const transform = (node: TreeNode) => this.labelProvider.getName(node);
this._filterResult = await this.fuzzySearch.filter({
items,
pattern,
transform
});
this._filteredNodes = this._filterResult.map(match => match.item);
this._filteredNodes = this._filterResult.map(({ item }) => {
this.addAllParentsToFilteredSet(item);
return item;
});
this.fireFilteredNodesChanged(this._filteredNodes);
return this._filteredNodes!.slice();
return this._filteredNodes.slice();
}

protected addAllParentsToFilteredSet(node: TreeNode): void {
let toAdd: TreeNode | undefined = node;
while (toAdd && !this._filteredNodesAndParents.has(toAdd.id)) {
this._filteredNodesAndParents.add(toAdd.id);
toAdd = toAdd.parent;
};
}

/**
Expand All @@ -87,6 +100,10 @@ export class TreeSearch implements Disposable {
return this.filteredNodesEmitter.event;
}

passesFilters(node: TreeNode): boolean {
return this._filteredNodesAndParents.has(node.id);
}

dispose(): void {
this.disposables.dispose();
}
Expand All @@ -108,5 +125,4 @@ export class TreeSearch implements Disposable {
length
};
}

}
17 changes: 12 additions & 5 deletions packages/core/src/browser/tree/tree-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
@postConstruct()
protected init(): void {
if (this.props.search) {
this.searchBox = this.searchBoxFactory({ ...SearchBoxProps.DEFAULT, showButtons: true });
this.searchBox = this.searchBoxFactory({ ...SearchBoxProps.DEFAULT, showButtons: true, showFilter: true });
this.toDispose.pushAll([
this.searchBox,
this.searchBox.onTextChange(async data => {
Expand All @@ -207,16 +207,19 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
this.model.selectPrevNode();
}
}),
this.searchBox.onFilterToggled(e => {
this.updateRows();
}),
this.treeSearch,
this.treeSearch.onFilteredNodesChanged(nodes => {
if (this.searchBox.isFiltering) {
this.updateRows();
}
const node = nodes.find(SelectableTreeNode.is);
if (node) {
this.model.selectNode(node);
}
}),
this.model.onExpansionChanged(() => {
this.searchBox.hide();
})
]);
}
this.toDispose.pushAll([
Expand Down Expand Up @@ -282,7 +285,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
pruneCollapsed: true,
pruneSiblings: true
})) {
if (TreeNode.isVisible(node)) {
if (this.shouldDisplayNode(node)) {
const parentDepth = depths.get(node.parent);
const depth = parentDepth === undefined ? 0 : TreeNode.isVisible(node.parent) ? parentDepth + 1 : parentDepth;
if (CompositeTreeNode.is(node)) {
Expand All @@ -300,6 +303,10 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
this.updateScrollToRow();
}

protected shouldDisplayNode(node: TreeNode): boolean {
return TreeNode.isVisible(node) && (!this.searchBox?.isFiltering || this.treeSearch.passesFilters(node));
}

/**
* Row index to ensure visibility.
* - Used to forcefully scroll if necessary.
Expand Down
3 changes: 3 additions & 0 deletions packages/scm/src/browser/scm-tree-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export class ScmTreeWidget extends TreeWidget {
}

set viewMode(id: 'tree' | 'list') {
// Close the search box because the structure of the tree will change dramatically
// and the search results will be out of date.
this.searchBox.hide();
this.model.viewMode = id;
}
get viewMode(): 'tree' | 'list' {
Expand Down

0 comments on commit e00d070

Please sign in to comment.