Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pagination, small refactor and correction #124

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8,759 changes: 8,759 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

97 changes: 62 additions & 35 deletions src/FilterItems/FilterItems.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import StyledFilterItems from './StyledFilterItems';
import { Filter } from '../types';
import { Filter, Pagination } from '../types';
import FilterItem from '../FilterItem';
import FilterizrOptions from '../FilterizrOptions/FilterizrOptions';
import {
Expand All @@ -11,6 +11,7 @@ import {
} from '../utils';
import { Destructible, Styleable } from '../types/interfaces';


export default class FilterItems implements Destructible, Styleable {
private filterItems: FilterItem[];
private styledFilterItems: StyledFilterItems;
Expand Down Expand Up @@ -48,25 +49,51 @@ export default class FilterItems implements Destructible, Styleable {
);
}

public getFiltered(filter: Filter): FilterItem[] {
const { searchTerm } = this.options;
const searchedFilterItems = this.search(this.filterItems, searchTerm);
if (filter === 'all') {
return searchedFilterItems;
}
return searchedFilterItems.filter((filterItem): boolean =>
this.shouldBeFiltered(filterItem.getCategories(), filter)
);
/**
* returns all item that are positive, this mean all the items that should be keeped.
*/
public getFiltered(filter: Filter, searchTerm : string, pagination : Pagination): FilterItem[] {
searchTerm = searchTerm || ""; //replace empty search term by empty string, who always match.
return this.filterItems.filter(this.getFilterPredicate(filter, searchTerm, pagination, true));
}

public getFilteredOut(filter: Filter): FilterItem[] {
const { searchTerm } = this.options;
return this.filterItems.filter((filterItem: FilterItem): boolean => {
const categories = filterItem.getCategories();
const shouldBeFiltered = this.shouldBeFiltered(categories, filter);
/**
* returns all item that are negative, this mean all the items that should be removed.
* the item is not keeped if the condition described in `getFiltered` is false.
*/
public getFilteredOut(filter: Filter, searchTerm : string, pagination : Pagination): FilterItem[] {
searchTerm = searchTerm || ""; //replace empty search term by empty string, who always match.
return this.filterItems.filter(this.getFilterPredicate(filter, searchTerm, pagination, false));
}

/**
* By extracting the structure of "getFiltered" and "getFilteredOut", we make it clearer the difference between them
* and prevent us of doing error between the two by reducing code duplication
*
* item is keeped if :
* (it's categorie match the current filter or the current filter is "all") and
* (it text match the search term or there is no search term) and
* (it's index match the current page range or there is no pagination)
*
* @param filter
* @param searchTerm
* @param inverse inverse the filtering. true => get all that are keeped. false => get all that are removed
*/
private getFilterPredicate(filter : Filter, searchTerm : string, pagination : Pagination, inverse : boolean) : (f : FilterItem) => boolean {
let acceptedElemCount = 0;
return (filterItem : FilterItem) : boolean => {
const shouldBeFiltered = this.shouldBeFiltered(filterItem.getCategories(), filter)
const contentsMatchSearch = filterItem.contentsMatchSearch(searchTerm);
return !shouldBeFiltered || !contentsMatchSearch;
});
const elementInRange = !pagination || (acceptedElemCount >= pagination.start && acceptedElemCount < pagination.end)
if(shouldBeFiltered && contentsMatchSearch) {
acceptedElemCount++;
}
if(inverse) {
return shouldBeFiltered && contentsMatchSearch && elementInRange
} else {
return !(shouldBeFiltered && contentsMatchSearch && elementInRange);
}
}
}

public sort(
Expand All @@ -84,21 +111,17 @@ export default class FilterItems implements Destructible, Styleable {
}

public shuffle(): void {
const filteredItems = this.getFiltered(this.options.filter);
const filteredItems = this.getFiltered(this.options.filter, this.options.searchTerm, null);

if (filteredItems.length > 1) {
const indicesBeforeShuffling = this.getFiltered(this.options.filter)
.map((filterItem): number => this.filterItems.indexOf(filterItem))
.slice();
const indicesBeforeShuffling = filteredItems
.map((filterItem): number => this.filterItems.indexOf(filterItem));

// Shuffle filtered items (until they have a new order)
let shuffledItems;
do {
shuffledItems = shuffle(filteredItems);
} while (filterItemArraysHaveSameSorting(filteredItems, shuffledItems));
{
shuffledItems = shuffle(filteredItems);
}

// Update filterItems to have them in the new shuffled order
shuffledItems.forEach((filterItem, index): void => {
Expand All @@ -122,18 +145,22 @@ export default class FilterItems implements Destructible, Styleable {
);
}

/**
* the filter system is mostly positive. you must have some or all of the filter term in your categorie to be still here.
* @returns {boolean}, true if the element should be keeped. the name is misleading.
*/
private shouldBeFiltered(categories: string[], filter: Filter): boolean {
const { multifilterLogicalOperator } = this.options.getRaw();
const isMultifilteringEnabled = Array.isArray(filter);

if (!isMultifilteringEnabled) {
return categories.includes(filter as string);
//by checking for filter === "all" here, we prevent us to forget to check it before calling "shouldBeFiltered"
if(filter === "all") {
return true
//By directly putting "isArray" in if condition, we can use typescript garde and we don't have to specify the type of filter with "as"
} else if(Array.isArray(filter)) {
//By using ternary operator, we reduce the number of "if" and "return" in the function, making it (arguably) clearer to read.
return this.options.getRaw().multifilterLogicalOperator === 'or' ?
!!intersection(categories, filter).length :
allStringsOfArray1InArray2(filter, categories)
} else {
return categories.includes(filter);
}

if (multifilterLogicalOperator === 'or') {
return !!intersection(categories, filter as string[]).length;
}

return allStringsOfArray1InArray2(filter as string[], categories);
}
}
53 changes: 50 additions & 3 deletions src/Filterizr/Filterizr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Spinner from '../Spinner';
import makeLayoutPositions from '../makeLayoutPositions';
import installAsJQueryPlugin from './installAsJQueryPlugin';

//@ts-ignore
const imagesLoaded = require('imagesloaded');

export default class Filterizr implements Destructible {
Expand Down Expand Up @@ -183,7 +184,8 @@ export default class Filterizr implements Destructible {

if (
'filter' in newOptions ||
'multifilterLomultifilterLogicalOperator' in newOptions
'multifilterLogicalOperator' in newOptions ||
'pagination' in newOptions
) {
this.filter(this.options.filter);
}
Expand All @@ -204,11 +206,56 @@ export default class Filterizr implements Destructible {
this.filter(this.options.filter);
}

/**
* page are 0 indexed.
* @param page the page where to go.
*/
public gotoPage(page : number) {
const opt = this.options.get();
if(opt.pagination) {
const nbrItem = this.filterItems.getFiltered(this.options.filter, this.options.searchTerm, null).length;
const lastPage = Math.floor(nbrItem / opt.pagination.pageSize);
if(page < 0) {
page = 0;
} else if(page > lastPage) {
page = lastPage;
}
opt.pagination.currentPage = page;
}
this.render();
}

public nextPage() {
const opt = this.options.get();
if(opt.pagination) {
const nbrItem = this.filterItems.getFiltered(this.options.filter, this.options.searchTerm, null).length;
const lastPage = Math.floor(nbrItem / opt.pagination.pageSize);
let page = opt.pagination.currentPage + 1;
if(page > lastPage) {
page = lastPage;
}
opt.pagination.currentPage = page;
}
this.render();
}

public previousPage() {
const opt = this.options.get();
if(opt.pagination) {
let page = opt.pagination.currentPage - 1;
if(page < 0) {
page = 0;
}
opt.pagination.currentPage = page;
}
this.render();
}

private render(): void {
const { filterContainer, filterItems, options } = this;
const itemsToFilterIn = filterItems.getFiltered(options.filter);
const itemsToFilterIn = filterItems.getFiltered(options.filter, options.searchTerm, options.getPageRange());

filterItems.getFilteredOut(options.filter).forEach((filterItem): void => {
filterItems.getFilteredOut(options.filter, options.searchTerm, options.getPageRange()).forEach((filterItem): void => {
filterItem.filterOut();
});

Expand Down
18 changes: 16 additions & 2 deletions src/FilterizrOptions/FilterizrOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { BaseOptions, RawOptions } from './../types/interfaces';
import { defaultOptions } from '.';
import { checkOptionForErrors, merge } from '../utils';
import ActiveFilter from '../ActiveFilter';
import { Filter } from '../types';
import { Filter, Pagination } from '../types';
import { PaginationOptions } from '../types/interfaces/BaseOptions';

export interface Options extends BaseOptions {
filter: ActiveFilter;
Expand Down Expand Up @@ -49,6 +50,13 @@ export default class FilterizrOptions {
this.options.searchTerm = searchTerm;
}

public getPageRange() : Pagination {
return this.options.pagination && {
start : this.options.pagination.pageSize * this.options.pagination.currentPage,
end : this.options.pagination.pageSize * (this.options.pagination.currentPage+1),
}
}

public get(): Options {
return this.options;
}
Expand Down Expand Up @@ -127,7 +135,13 @@ export default class FilterizrOptions {
);
checkOptionForErrors('searchTerm', options.searchTerm, 'string');
checkOptionForErrors('setupControls', options.setupControls, 'boolean');

checkOptionForErrors('pagination', options.pagination, 'object');
if(options.pagination) {
checkOptionForErrors('pagination.pageSize', options.pagination.pageSize, 'number');
checkOptionForErrors('pagination.currentPage', options.pagination.currentPage, 'number');
if(options.pagination.pageSize < 0) {options.pagination.pageSize = 0}
if(options.pagination.currentPage < 0) {options.pagination.currentPage = 0}
}
return options;
}
}
1 change: 1 addition & 0 deletions src/FilterizrOptions/defaultOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const defaultOptions: RawOptions = {
'z-index': 2,
},
},
pagination : null
};

export default defaultOptions;
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export type Layout =
| 'sameWidth'
| 'sameSize'
| 'packed';
export type Pagination = {start : number, end : number};
6 changes: 6 additions & 0 deletions src/types/interfaces/BaseOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@ export interface BaseOptions {
searchTerm?: string;
setupControls?: boolean;
spinner?: SpinnerOptions;
pagination? : PaginationOptions
}

export interface PaginationOptions {
pageSize : number;
currentPage : number;
}
6 changes: 3 additions & 3 deletions src/utils/setStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
* Set inline styles on an HTML node
* @param {HTMLElement} node - HTML node
* @param {Object} styles - object with styles
* @returns {undefined}
* @returns {void}
*/
export function setStyles(node: Element, styles: any): void {
export function setStyles(node: HTMLElement, styles: any): void {
Object.entries(styles).forEach(([key, value]): void => {
((node as HTMLElement).style as any)[key] = value;
(node.style as any)[key] = value;
});
}
11 changes: 6 additions & 5 deletions src/utils/shuffle.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/**
* Fisher-Yates shuffle ES6 non-mutating implementation.
* @param {Array} array the array to shuffle
* @return {Array} shuffled array without mutating the initial array.
* @param {Array<T>} array the array to shuffle
* @return {Array<T>} shuffled array without mutating the initial array, but with the same element reference.
* @template {T}
*/
export const shuffle = (array: any[]): any[] => {
// perform deep clone on array to mutate
let cloned = array.slice(0);
export const shuffle = <T>(array: T[]): T[] => {
// perform shallow clone on array to mutate
let cloned = array.slice();
// array to return
let randomizedArray = [];
// perform shuffle
Expand Down
67 changes: 67 additions & 0 deletions tests/FilterItems.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import defaultOptions from '../src/FilterizrOptions/defaultOptions';
import { RawOptions } from '../src/types/interfaces';
// import test utils
import * as $ from 'jquery';
import { fakeDom } from './testSetup';
// import items to be tested
import Filterizr from '../src/Filterizr';
import FilterContainer from '../src/FilterContainer';
import FilterItem from '../src/FilterItem';
import FilterItems from '../src/FilterItems';

// General setup
(window as any).$ = $;

jest.mock('fast-memoize', () => ({ default: (a: any) => a }));

// Test suite for Filterizr
describe('Filterizr', () => {
// Basic setup before all tests
let filterizr: Filterizr;
let filterContainer: FilterContainer;
let filterItems : FilterItems;

beforeEach(() => {
$('body').html(fakeDom);
filterizr = new Filterizr('.filtr-container', defaultOptions);
filterContainer = filterizr['filterContainer'];
filterItems = filterContainer.filterItems;
});

describe('#filteredIn-pagination', () => {
it("should return all element when no filter nor pagination", () => {
expect(
filterItems.getFiltered("all", "", null).length
).toEqual(9);
})
it('filter should return up to the end of the range', () => {
const allItem = filterItems.getFiltered("all", "", null)
const range = filterItems.getFiltered("all", "", {start : 0, end : 3});
expect(range.length).toEqual(3);
expect(range).toStrictEqual(allItem.slice(0, 3))
});

it('should skip the first when range start after the first', () => {
const allItem = filterItems.getFiltered("all", "", null);
const range36 = filterItems.getFiltered("all", "", {start : 3, end : 6});
expect(range36.length).toEqual(3);
expect(range36).toStrictEqual(allItem.slice(3, 6))
});

it('should return the good number of element event if filtered', () => {
const allItem = filterItems.getFiltered("all", "", null);
const filteredRange = filterItems.getFiltered("4", "", {start : 2, end : 4});
expect(filteredRange[0]).toEqual(allItem[5])
expect(filteredRange[1]).toEqual(allItem[7])
expect(filteredRange.length).toEqual(2);
});

it('should work with search', () => {
const allItem = filterItems.getFiltered("all", "", null);
const filteredRange = filterItems.getFiltered("all", "city", {start : 1, end : 2});
expect(filteredRange[0]).toEqual(allItem[6])
expect(filteredRange.length).toEqual(1);
})
});
});
Loading