Skip to content

Commit

Permalink
Merge pull request #338 from gund/fix-filter-pipe
Browse files Browse the repository at this point in the history
[Fix] Filter Pipe
  • Loading branch information
softsimon authored Nov 29, 2017
2 parents f12ed6c + 53b8aaa commit f24a25b
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 240 deletions.
25 changes: 12 additions & 13 deletions src/dropdown/dropdown.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/operator/takeUntil';

import {
Component,
Expand All @@ -14,7 +13,7 @@ import {
OnDestroy,
OnInit,
Output,
SimpleChanges
SimpleChanges,
} from '@angular/core';
import {
AbstractControl,
Expand Down Expand Up @@ -188,12 +187,12 @@ export class MultiselectDropdown implements OnInit, OnChanges, DoCheck, OnDestro

this.filterControl.valueChanges
.takeUntil(this.destroyed$)
.subscribe(function () {
.subscribe(() => {
this.updateRenderItems();
if (this.settings.isLazyLoad) {
this.load();
}
}.bind(this));
});
}

ngOnChanges(changes: SimpleChanges) {
Expand Down Expand Up @@ -418,13 +417,13 @@ export class MultiselectDropdown implements OnInit, OnChanges, DoCheck, OnDestro

addChecks(options) {
let checkedOptions = options
.filter(function(option: IMultiSelectOption) {
if (this.model.indexOf(option.id) === -1 && !(this.settings.ignoreLabels && option.isLabel)) {
this.onAdded.emit(option.id);
return true;
}
return false;
}.bind(this)).map((option: IMultiSelectOption) => option.id);
.filter((option: IMultiSelectOption) => {
if (this.model.indexOf(option.id) === -1 && !(this.settings.ignoreLabels && option.isLabel)) {
this.onAdded.emit(option.id);
return true;
}
return false;
}).map((option: IMultiSelectOption) => option.id);
this.model = this.model.concat(checkedOptions);
}

Expand Down Expand Up @@ -464,10 +463,10 @@ export class MultiselectDropdown implements OnInit, OnChanges, DoCheck, OnDestro
if (this.searchFilterApplied()) {
if (this.checkAllSearchRegister.has(this.filterControl.value)) {
this.checkAllSearchRegister.delete(this.filterControl.value);
this.checkAllSearchRegister.forEach(function(searchTerm) {
this.checkAllSearchRegister.forEach((searchTerm) => {
let filterOptions = this.applyFilters(this.options.filter(option => unCheckedOptions.includes(option.id)), searchTerm);
this.addChecks(filterOptions);
}.bind(this));
});
}
} else {
this.checkAllSearchRegister.clear();
Expand Down
106 changes: 71 additions & 35 deletions src/dropdown/search-filter.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,127 @@
import { Pipe, PipeTransform } from '@angular/core';

import { IMultiSelectOption } from './types';

interface StringHashMap<T> {
[k: string]: T;
}

@Pipe({
name: 'searchFilter'
})
export class MultiSelectSearchFilter implements PipeTransform {

private _lastOptions: IMultiSelectOption[];
private _searchCache: { [k: string]: IMultiSelectOption[] } = {};
private _searchCacheInclusive: { [k: string]: boolean | number } = {};

transform(options: Array<IMultiSelectOption>, str: string, limit = 0, renderLimit = 0): Array<IMultiSelectOption> {
str = (str || '').toLowerCase();
private _searchCache: StringHashMap<IMultiSelectOption[]> = {};
private _searchCacheInclusive: StringHashMap<boolean | number> = {};
private _prevSkippedItems: StringHashMap<number> = {};

transform(
options: IMultiSelectOption[],
str = '',
limit = 0,
renderLimit = 0
): IMultiSelectOption[] {
str = str.toLowerCase();

// Drop cache because options were updated
if (options !== this._lastOptions) {
this._lastOptions = options;
this._searchCache = {};
this._searchCacheInclusive = {};
this._prevSkippedItems = {};
}

const filteredOpts = this._searchCache.hasOwnProperty(str)
? this._searchCache[str]
: this._doSearch(options, str, limit);

const isUnderLimit = options.length <= limit;

if (this._searchCache.hasOwnProperty(str)) {
return isUnderLimit ? this._searchCache[str] : this._limitRenderedItems(this._searchCache[str], renderLimit);
return isUnderLimit
? filteredOpts
: this._limitRenderedItems(filteredOpts, renderLimit);
}

private _getSubsetOptions(
options: IMultiSelectOption[],
prevOptions: IMultiSelectOption[],
prevSearchStr: string
) {
const prevInclusiveOrIdx = this._searchCacheInclusive[prevSearchStr];

if (prevInclusiveOrIdx === true) {
// If have previous results and it was inclusive, do only subsearch
return prevOptions;
} else if (typeof prevInclusiveOrIdx === 'number') {
// Or reuse prev results with unchecked ones
return [...prevOptions, ...options.slice(prevInclusiveOrIdx)];
}

return options;
}

private _doSearch(options: IMultiSelectOption[], str: string, limit: number) {
const prevStr = str.slice(0, -1);
const prevResults = this._searchCache[prevStr];
const prevResultShift = this._prevSkippedItems[prevStr] || 0;

if (prevResults) {
const prevInclusiveOrIdx = this._searchCacheInclusive[prevStr];

if (prevInclusiveOrIdx === true) {
// If have previous results and it was inclusive, do only subsearch
options = prevResults;
} else if (typeof prevInclusiveOrIdx === 'number') {
// Or reuse prev results with unchecked ones
options = [...prevResults, ...options.slice(prevInclusiveOrIdx)];
}
options = this._getSubsetOptions(options, prevResults, prevStr);
}

const optsLength = options.length;
const maxFound = limit > 0 ? Math.min(limit, optsLength) : optsLength;
const filteredOpts = [];

const regexp = new RegExp(this._escapeRegExp(str), 'i');
const filteredOpts: IMultiSelectOption[] = [];

let i = 0, founded = 0, removedFromPrevResult = 0;

const matchPredicate = (option: IMultiSelectOption) => regexp.test(option.name),
getChildren = (option: IMultiSelectOption) => options.filter(child => child.parentId === option.id),
getParent = (option: IMultiSelectOption) => options.find(parent => option.parentId === parent.id);
const doesOptionMatch = (option: IMultiSelectOption) => regexp.test(option.name);
const getChildren = (option: IMultiSelectOption) =>
options.filter(child => child.parentId === option.id);
const getParent = (option: IMultiSelectOption) =>
options.find(parent => option.parentId === parent.id);
const foundFn = (item: any) => { filteredOpts.push(item); founded++; };
const notFoundFn = prevResults ? () => removedFromPrevResult++ : () => { };

let i = 0, founded = 0;
for (; i < optsLength && founded < maxFound; ++i) {
const option = options[i];
const directMatch = regexp.test(option.name);
const directMatch = doesOptionMatch(option);

if (directMatch) {
filteredOpts.push(option);
founded++;
foundFn(option);
continue;
}

if (typeof (option.parentId) === 'undefined') {
const childrenMatch = getChildren(option).some(matchPredicate);
if (typeof option.parentId === 'undefined') {
const childrenMatch = getChildren(option).some(doesOptionMatch);

if (childrenMatch) {
filteredOpts.push(option);
founded++;
foundFn(option);
continue;
}
}

if (typeof (option.parentId) !== 'undefined') {
const parentMatch = matchPredicate(getParent(option));
if (typeof option.parentId !== 'undefined') {
const parentMatch = doesOptionMatch(getParent(option));

if (parentMatch) {
filteredOpts.push(option);
founded++;
foundFn(option);
continue;
}
}

notFoundFn();
}

const totalIterations = i + prevResultShift;

this._searchCache[str] = filteredOpts;
this._searchCacheInclusive[str] = i === optsLength || i + 1;
this._searchCacheInclusive[str] = i === optsLength || totalIterations;
this._prevSkippedItems[str] = removedFromPrevResult + prevResultShift;

return isUnderLimit ? filteredOpts : this._limitRenderedItems(filteredOpts, renderLimit);
return filteredOpts;
}

private _limitRenderedItems<T>(items: T[], limit: number): T[] {
Expand All @@ -96,4 +131,5 @@ export class MultiSelectSearchFilter implements PipeTransform {
private _escapeRegExp(str: string): string {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}

}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es6", "dom"],
"lib": ["es6", "dom", "es7"],
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
Expand Down
Loading

0 comments on commit f24a25b

Please sign in to comment.