Skip to content

Commit

Permalink
feat(filter): add new SliderRange Filter
Browse files Browse the repository at this point in the history
  • Loading branch information
Ghislain Beaulac authored and Ghislain Beaulac committed Aug 7, 2019
1 parent 9784096 commit 26dc63c
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 19 deletions.
36 changes: 24 additions & 12 deletions src/app/examples/grid-range.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AngularGridInstance, Column, FieldType, Filters, Formatter, Formatters,
import { CustomInputFilter } from './custom-inputFilter';
import * as moment from 'moment-mini';

const NB_ITEMS = 500;
const NB_ITEMS = 1000;

function randomBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
Expand All @@ -28,7 +28,7 @@ export class GridRangeComponent implements OnInit {
<br/>
<ul class="small">
<li>All input filters support the following operators: (>, >=, <, <=, <>, !=, =, ==, *)
<li>All Filters supporting range will be using the 2 dots (..) to represent a range, even for dates to be consistent when using the "presets"</li>
<li>All input filters also support range search by using the 2 dots (..) to represent a range, even for dates to be consistent when using the "presets"</li>
<ul>
<li>For the range in a text input filters, you can use 2 dots (..) to represent a range</li>
<li>example: type "5..10" to filter between 5 and 10 (non-inclusive)</li>
Expand Down Expand Up @@ -73,18 +73,27 @@ export class GridRangeComponent implements OnInit {
}
},
{
id: 'duration', field: 'duration', headerKey: 'DURATION', sortable: true,
formatter: Formatters.percentCompleteBar, minWidth: 100,
filterable: true,
filter: { model: Filters.slider, /* operator: '>=',*/ }
},
{
id: 'complete', name: '% Complete', field: 'percentComplete', headerKey: 'PERCENT_COMPLETE', minWidth: 70, type: FieldType.number, sortable: true,
id: 'complete', name: '% Complete', field: 'percentComplete', headerKey: 'PERCENT_COMPLETE', minWidth: 70,
type: FieldType.number,
sortable: true,
filterable: true, filter: {
model: Filters.input,
operator: OperatorType.rangeExclusive // defaults to exclusive
}
},
{
id: 'duration', field: 'duration', headerKey: 'DURATION', sortable: true,
formatter: Formatters.percentCompleteBar, minWidth: 100,
type: FieldType.number,
filterable: true,
filter: {
model: Filters.sliderRange,
minValue: 0,
maxValue: 100,
/* operator: OperatorType.rangeExclusive, */
params: { valueStep: 5 } // you can provide an optional value step, 1 is the default
}
},
{
id: 'start', name: 'Start', field: 'start', headerKey: 'START', formatter: Formatters.dateIso, sortable: true, minWidth: 75, exportWithFormatter: true,
type: FieldType.date, filterable: true, filter: { model: Filters.compoundDate }
Expand Down Expand Up @@ -112,6 +121,9 @@ export class GridRangeComponent implements OnInit {
}
];

const presetLowestDay = moment().add(-2, 'days').format('YYYY-MM-DD');
const presetHighestDay = moment().add(20, 'days').format('YYYY-MM-DD');

this.gridOptions = {
autoResize: {
containerId: 'demo-container',
Expand All @@ -126,14 +138,14 @@ export class GridRangeComponent implements OnInit {
// use columnDef searchTerms OR use presets as shown below
presets: {
filters: [
// { columnId: 'duration', searchTerms: [10, 220] },
{ columnId: 'duration', searchTerms: ['15..75'] },
{ columnId: 'complete', searchTerms: ['15..75'] },
{ columnId: 'finish', operator: 'RangeInclusive', searchTerms: [`${moment().add(-2, 'days').format('YYYY-MM-DD')}..${moment().add(20, 'days').format('YYYY-MM-DD')}`] },
{ columnId: 'finish', operator: 'RangeInclusive', searchTerms: [`${presetLowestDay}..${presetHighestDay}`] },
// { columnId: 'effort-driven', searchTerms: [true] }
],
sorters: [
{ columnId: 'complete', direction: 'ASC' },
{ columnId: 'duration', direction: 'DESC' },
{ columnId: 'complete', direction: 'ASC' }
],
}
};
Expand Down
6 changes: 5 additions & 1 deletion src/app/modules/angular-slickgrid/filters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { NativeSelectFilter } from './nativeSelectFilter';
import { RangeDateFilter } from './rangeDateFilter';
import { SingleSelectFilter } from './singleSelectFilter';
import { SliderFilter } from './sliderFilter';
import { SliderRangeFilter } from './sliderRangeFilter';

export const Filters = {
/** AutoComplete Filter (using jQuery UI autocomplete feature) */
Expand Down Expand Up @@ -67,6 +68,9 @@ export const Filters = {
/** Single Select filter, which uses 3rd party lib "multiple-select.js" */
singleSelect: SingleSelectFilter,

/** Slider Filter */
/** Slider Filter (only 1 value) */
slider: SliderFilter,

/** Slider Range Filter, uses jQuery UI Range Slider (2 values, lowest/highest search range) */
sliderRange: SliderRangeFilter,
};
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class RangeDateFilter implements Filter {

/** Getter for the Filter Operator */
get operator(): OperatorType | OperatorString {
return this._operator || OperatorType.rangeInclusive;
return this._operator || OperatorType.rangeExclusive;
}

/**
Expand Down
202 changes: 202 additions & 0 deletions src/app/modules/angular-slickgrid/filters/sliderRangeFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import {
Column,
ColumnFilter,
Filter,
FilterArguments,
FilterCallback,
OperatorType,
OperatorString,
SearchTerm,
} from '../models/index';

// using external non-typed js libraries
declare var $: any;

const DEFAULT_MIN_VALUE = 0;
const DEFAULT_MAX_VALUE = 100;
const DEFAULT_STEP = 1;

/** A Slider Range Filter which uses jQuery UI, this is only meant to be used as a range filter (with 2 handles lowest & highest values) */
export class SliderRangeFilter implements Filter {
private _clearFilterTriggered = false;
private _shouldTriggerQuery = true;
private _elementRangeInputId: string;
private _elementRangeOutputId: string;
private $filterElm: any;
grid: any;
searchTerms: SearchTerm[];
columnDef: Column;
callback: FilterCallback;

/** Getter for the Filter Generic Params */
private get filterParams(): any {
return this.columnDef && this.columnDef.filter && this.columnDef.filter.params || {};
}

/** Getter for the `filter` properties */
private get filterProperties(): ColumnFilter {
return this.columnDef && this.columnDef.filter;
}

get operator(): OperatorType | OperatorString {
return (this.columnDef && this.columnDef.filter && this.columnDef.filter.operator) || OperatorType.rangeExclusive;
}

/**
* Initialize the Filter
*/
init(args: FilterArguments) {
if (!args) {
throw new Error('[Angular-SlickGrid] A filter must always have an "init()" with valid arguments.');
}
this.grid = args.grid;
this.callback = args.callback;
this.columnDef = args.columnDef;
this.searchTerms = args.searchTerms || [];

// define the input & slider number IDs
this._elementRangeInputId = `rangeInput_${this.columnDef.field}`;
this._elementRangeOutputId = `rangeOutput_${this.columnDef.field}`;

// filter input can only have 1 search term, so we will use the 1st array index if it exist
const searchTerm = (Array.isArray(this.searchTerms) && this.searchTerms[0]) || '';

// step 1, create the DOM Element of the filter & initialize it if searchTerm is filled
this.$filterElm = this.createDomElement(searchTerm);

// step 3, subscribe to the change event and run the callback when that happens
// also add/remove "filled" class for styling purposes
this.$filterElm.change((e: any) => {
const value = e && e.target && e.target.value || '';
if (this._clearFilterTriggered) {
this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery });
this.$filterElm.removeClass('filled');
} else {
value === '' ? this.$filterElm.removeClass('filled') : this.$filterElm.addClass('filled');
this.callback(e, { columnDef: this.columnDef, operator: this.operator, searchTerms: [value], shouldTriggerQuery: this._shouldTriggerQuery });
}
// reset both flags for next use
this._clearFilterTriggered = false;
this._shouldTriggerQuery = true;
});

// if user chose to display the slider number on the right side, then update it every time it changes
// we need to use both "input" and "change" event to be all cross-browser
if (!this.filterParams.hideSliderNumber) {
this.$filterElm.on('input change', (e: { target: HTMLInputElement }) => {
const value = e && e.target && e.target.value || '';
if (value) {
document.getElementById(this._elementRangeOutputId).innerHTML = value;
}
});
}
}

/**
* Clear the filter value
*/
clear(shouldTriggerQuery = true) {
if (this.$filterElm) {
this._clearFilterTriggered = true;
this._shouldTriggerQuery = shouldTriggerQuery;
this.searchTerms = [];
const lowestValue = this.filterParams.hasOwnProperty('sliderStartValue') ? this.filterParams.sliderStartValue : DEFAULT_MIN_VALUE;
const highestValue = this.filterParams.hasOwnProperty('sliderEndValue') ? this.filterParams.sliderEndValue : DEFAULT_MAX_VALUE;
this.$filterElm.slider('values', [lowestValue, highestValue]);
// this.$filterElm.children('input').val(clearedValue);
// this.$filterElm.children('div.input-group-addon.input-group-append').children().html(clearedValue);
// this.$filterElm.trigger('change');
this.callback(null, { columnDef: this.columnDef, clearFilterTriggered: true, shouldTriggerQuery });
this.$filterElm.removeClass('filled');
}
}

/**
* destroy the filter
*/
destroy() {
if (this.$filterElm) {
this.$filterElm.off('change').remove();
}
}

/**
* Set value(s) on the DOM element
*/
setValues(values: SearchTerm) {
if (values) {
const searchTerm = (Array.isArray(values) && typeof values[0] === 'string') ? values[0] : '';
const sliderValues = ((searchTerm as string).indexOf('..') >= 0) ? (searchTerm as string).split('..') : searchTerm;
if (Array.isArray(sliderValues) && sliderValues.length === 2) {
this.$filterElm.slider('values', [sliderValues[0], sliderValues[1]]);
} else {
this.clear(true);
}
}
}

//
// private functions
// ------------------

/**
* From the html template string, create a DOM element
* @param searchTerm optional preset search terms
*/
private createDomElement(searchTerm?: SearchTerm) {
const fieldId = this.columnDef && this.columnDef.id;
const $headerElm = this.grid.getHeaderRowColumn(fieldId);
$($headerElm).empty();

// create the DOM element & add an ID and filter class
const $filterElm = $(`<div class="search-filter filter-${fieldId}"></div>`);
const $filterContainerElm = $(`<div class="slider-range-container form-control">`);
$filterElm.appendTo($filterContainerElm);
const searchTermInput = (searchTerm || '0') as string;

const minValue = this.filterProperties.hasOwnProperty('minValue') ? this.filterProperties.minValue : DEFAULT_MIN_VALUE;
const maxValue = this.filterProperties.hasOwnProperty('maxValue') ? this.filterProperties.maxValue : DEFAULT_MAX_VALUE;
const defaultStartValue = this.filterParams.hasOwnProperty('sliderStartValue') ? this.filterParams.sliderStartValue : minValue;
const defaultEndValue = this.filterParams.hasOwnProperty('sliderEndValue') ? this.filterParams.sliderEndValue : maxValue;
const step = this.filterProperties.hasOwnProperty('valueStep') ? this.filterProperties.valueStep : DEFAULT_STEP;

$filterElm.slider({
range: true,
min: minValue,
max: maxValue,
step,
values: [defaultStartValue, defaultEndValue],
slide: (e: any, ui: { handle: HTMLElement; handleIndex: number; value: number; values: number[]; }) => {
const value = ui.values.join('..');

if (this._clearFilterTriggered) {
this.callback(e, { columnDef: this.columnDef, clearFilterTriggered: this._clearFilterTriggered, shouldTriggerQuery: this._shouldTriggerQuery });
this.$filterElm.removeClass('filled');
} else {
value === '' ? this.$filterElm.removeClass('filled') : this.$filterElm.addClass('filled');
this.callback(e, { columnDef: this.columnDef, operator: this.operator, searchTerms: [value], shouldTriggerQuery: this._shouldTriggerQuery });
}
// reset both flags for next use
this._clearFilterTriggered = false;
this._shouldTriggerQuery = true;
}
});

// $filterElm.children('input').val(searchTermInput);
// $filterElm.children('div.input-group-addon.input-group-append').children().html(searchTermInput);
// $filterElm.attr('id', `filter-${fieldId}`);
// $filterElm.data('columnId', fieldId);

// if there's a search term, we will add the "filled" class for styling purposes
if (searchTerm) {
$filterContainerElm.addClass('filled');
}

// append the new DOM element to the header row
if ($filterContainerElm && typeof $filterContainerElm.appendTo === 'function') {
$filterContainerElm.appendTo($headerElm);
}

return $filterElm;
}
}
18 changes: 17 additions & 1 deletion src/app/modules/angular-slickgrid/styles/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ $draggable-group-column-icon-height: 9px !default;
$draggable-group-column-icon-width: 9px !default;
$draggable-group-column-icon-margin-left: 4px !default;

/* Input Range Filter */
/* Input Slider Filter (vanilla html) */
$slider-filter-border: 1px solid #ccc !default;
$slider-filter-bgcolor: #eee !default;
$slider-filter-runnable-track-bgcolor: #ddd !default;
Expand All @@ -271,12 +271,28 @@ $slider-filter-runnable-track-padding: 0 6px !default;
$slider-filter-fill-lower-color: #ddd !default; /* ms only */
$slider-filter-fill-focus-lower-color: #aaa !default; /* ms only */
$slider-filter-height: $header-input-height !default;
$slider-filter-thumb-cursor: pointer !default;
$slider-filter-thumb-color: rgb(201, 219, 203) !default;
$slider-filter-thumb-size: 14px !default;
$slider-filter-thumb-border: 1px solid darken($slider-filter-thumb-color, 15%) !default;
$slider-filter-number-padding: 4px 8px !default;
$slider-filter-number-font-size: ($font-size-base-value - 1px) !default;

/* Input Range Slider Filter (with jQuery UI) */
$slider-range-filter-height: $slider-filter-height !default;
$slider-range-filter-border: $slider-filter-border !default;
$slider-range-filter-thumb-color: $slider-filter-thumb-color !default;
$slider-range-filter-thumb-border: $slider-filter-thumb-border !default;
$slider-range-filter-thumb-border-radius: 50% !default;
$slider-range-filter-thumb-cursor: $slider-filter-thumb-cursor !default;
$slider-range-filter-thumb-size: $slider-filter-thumb-size !default;
$slider-range-filter-thumb-top: -5px !default;
$slider-range-filter-thumb-margin-left: -7px !default;
$slider-range-filter-runnable-track-top: 45% !default;
$slider-range-filter-runnable-track-height: $slider-filter-runnable-track-height !default;
$slider-range-filter-bgcolor: $slider-filter-bgcolor !default;
$slider-range-filter-padding: 0 12px !default;

/* Multiple-Select Filter */
$multiselect-input-filter-border: 1px solid #ccc !default;
$multiselect-input-filter-font-family: "Helvetica Neue", Helvetica, Arial !default;
Expand Down
3 changes: 0 additions & 3 deletions src/app/modules/angular-slickgrid/styles/slick-bootstrap.scss
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,6 @@
border-left: $viewport-border-left;
border-right: $viewport-border-right;
}
.ui-state-default {
border: 0;
}

.grid-canvas {
.slick-row {
Expand Down

0 comments on commit 26dc63c

Please sign in to comment.