Skip to content

Commit

Permalink
[feature] Compound Filters (input & date) (#32)
Browse files Browse the repository at this point in the history
* fix(build): refactored code to fix build with compound filters

* fix(filter): compound input filter on string type not working correctly

* fix(filter): CompoundDate Filter should add a time picker when found 'h'

* fix(filter): CompoundDateFilter time picker should be delayed
- When using DatePicker with a TimePicker, we should simulate a user typing with delay to avoid too much backend traffic when chaging time

* fix(build): refactored to fix the build

* fix(import): fix latest version jquery-ui import

* feat(demo): update demo samples
  • Loading branch information
ghiscoding committed Mar 20, 2018
1 parent c11eff4 commit 763e766
Show file tree
Hide file tree
Showing 51 changed files with 814 additions and 21,998 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ bower_components
npm-debug.log
package-lock.json
yarn.lock
yarn-error.log
1 change: 1 addition & 0 deletions aurelia-slickgrid/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ node_modules

# Compiled files
scripts
yarn-error.log
6 changes: 6 additions & 0 deletions aurelia-slickgrid/assets/i18n/en/aurelia-slickgrid.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@
"CLEAR_ALL_FILTERS": "Clear All Filters",
"COLUMNS": "Columns",
"COMMANDS": "Commands",
"CONTAINS": "Contains",
"ENDS_WITH": "Ends With",
"EQUALS": "Equals",
"EXPORT_TO_CSV": "Export in CSV format",
"EXPORT_TO_TAB_DELIMITED": "Export in Text format (Tab delimited)",
"FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} of {{totalItems}} items",
"FORCE_FIT_COLUMNS": "Force fit columns",
"GROUP_BY": "Group by",
"IN_COLLECTION_SEPERATED_BY_COMMA": "Search items in a collection, must be separated by a comma (a,b)",
"ITEMS": "Items",
"ITEMS_PER_PAGE": "items per page",
"NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Search items not in a collection, must be separated by a comma (a,b)",
"OF": "of",
"PAGE": "Page",
"PAGE_X_OF_Y": "page {{x}} of {{y}}",
"REFRESH_DATASET": "Refresh Dataset",
"SELECT_ALL": "Select All",
"STARTS_WITH": "Starts With",
"SYNCHRONOUS_RESIZE": "Synchronous resize",
"TOGGLE_FILTER_ROW": "Toggle Filter Row",
"X_OF_Y_SELECTED": "# of % selected",
Expand Down
6 changes: 6 additions & 0 deletions aurelia-slickgrid/assets/i18n/fr/aurelia-slickgrid.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@
"CLEAR_ALL_FILTERS": "Effacer tous les filtres",
"COLUMNS": "Colonnes",
"COMMANDS": "Commandes",
"CONTAINS": "Contient",
"ENDS_WITH": "Se termine par",
"EQUALS": "Égale",
"EXPORT_TO_CSV": "Exporter en format CSV",
"EXPORT_TO_TAB_DELIMITED": "Exporter en format texte (délimité par tabulation)",
"FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} de {{totalItems}} éléments",
"FORCE_FIT_COLUMNS": "Ajustement forcé des colonnes",
"GROUP_BY": "Grouper par",
"IN_COLLECTION_SEPERATED_BY_COMMA": "Recherche incluant certain éléments d'une collection, doit être séparé par une virgule (a,b)",
"ITEMS": "Éléments",
"ITEMS_PER_PAGE": "éléments par page",
"NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Recherche excluant certain éléments d'une collection, doit être séparé par une virgule (a,b)",
"OF": "de",
"PAGE": "Page",
"PAGE_X_OF_Y": "page {{x}} de {{y}}",
"REFRESH_DATASET": "Rafraîchir les données",
"SELECT_ALL": "Sélectionner tout",
"STARTS_WITH": "Commence par",
"SYNCHRONOUS_RESIZE": "Redimension synchrone",
"TOGGLE_FILTER_ROW": "Basculer la ligne des filtres",
"X_OF_Y_SELECTED": "# de % sélectionné",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GridStateChange } from './models/gridStateChange.interface';
// import 3rd party vendor libs
import 'jquery-ui-dist/jquery-ui.min.js';
import 'jquery-ui-dist/jquery-ui';
import 'slickgrid/lib/jquery.event.drag-2.3.0';

import 'slickgrid/slick.core';
Expand Down
7 changes: 5 additions & 2 deletions aurelia-slickgrid/src/aurelia-slickgrid/editors/dateEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ export class DateEditor implements Editor {
this.defaultDate = this.args.item[this.args.column.field] || null;
const inputFormat = mapFlatpickrDateFormatWithFieldType(this.args.column.type || FieldType.dateIso);
const outputFormat = mapFlatpickrDateFormatWithFieldType(this.args.column.outputType || FieldType.dateUtc);
const currentLocale = this.getCurrentLocale(this.args.column, gridOptions);
let currentLocale = this.getCurrentLocale(this.args.column, gridOptions);
if (currentLocale.length > 2) {
currentLocale = currentLocale.substring(0, 2);
}

const pickerOptions: any = {
defaultDate: this.defaultDate,
Expand All @@ -43,7 +46,7 @@ export class DateEditor implements Editor {
}

getCurrentLocale(columnDef: Column, gridOptions: GridOption) {
const params = columnDef.params || {};
const params = gridOptions.params || columnDef.params || {};
if (params.i18n && params.i18n instanceof I18N) {
return params.i18n.getLocale();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import { testFilterCondition } from './filterUtilities';
import * as moment from 'moment';

export const dateUtcFilterCondition: FilterCondition = (options: FilterConditionOption) => {
if (!options.filterSearchType) {
throw new Error('Date UTC filter is a special case and requires a filterSearchType to be provided in the column option, for example: { filterable: true, type: FieldType.dateUtc, filterSearchType: FieldType.dateIso }');
}

const searchDateFormat = mapMomentDateFormatWithFieldType(options.filterSearchType);
const searchDateFormat = mapMomentDateFormatWithFieldType(options.filterSearchType || options.fieldType);
if (!moment(options.cellValue, moment.ISO_8601).isValid() || !moment(options.searchTerm, searchDateFormat, true).isValid()) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FilterCondition, FilterConditionOption } from '../models/index';
import { FilterCondition, FilterConditionOption, OperatorType } from '../models/index';
import { testFilterCondition } from './filterUtilities';

export const stringFilterCondition: FilterCondition = (options: FilterConditionOption) => {
Expand All @@ -9,9 +9,9 @@ export const stringFilterCondition: FilterCondition = (options: FilterConditionO
const cellValue = options.cellValue.toLowerCase();
const searchTerm = (typeof options.searchTerm === 'string') ? options.searchTerm.toLowerCase() : options.searchTerm;

if (options.operator === '*') {
if (options.operator === '*' || options.operator === OperatorType.endsWith) {
return cellValue.endsWith(searchTerm);
} else if (options.operator === '' && options.cellValueLastChar === '*') {
} else if ((options.operator === '' && options.cellValueLastChar === '*') || options.operator === OperatorType.startsWith) {
return cellValue.startsWith(searchTerm);
} else if (options.operator === '') {
return cellValue.includes(searchTerm);
Expand Down
249 changes: 249 additions & 0 deletions aurelia-slickgrid/src/aurelia-slickgrid/filters/compoundDateFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { inject } from 'aurelia-framework';
import { I18N } from 'aurelia-i18n';
import { mapFlatpickrDateFormatWithFieldType } from '../services/utilities';
import {
Column,
FieldType,
Filter,
FilterArguments,
FilterCallback,
FilterType,
GridOption,
OperatorString,
OperatorType,
SearchTerm
} from './../models/index';
import * as flatpickr from 'flatpickr';
import * as $ from 'jquery';

@inject(I18N)
export class CompoundDateFilter implements Filter {
private $filterElm: any;
private $filterInputElm: any;
private $selectOperatorElm: any;
private _currentValue: string;
flatInstance: any;
grid: any;
gridOptions: GridOption;
operator: OperatorType | OperatorString | undefined;
searchTerm: SearchTerm | undefined;
columnDef: Column;
callback: FilterCallback;
filterType = FilterType.compoundDate;

constructor(private i18n: I18N) { }

/**
* Initialize the Filter
*/
init(args: FilterArguments) {
if (args) {
this.grid = args.grid;
this.callback = args.callback;
this.columnDef = args.columnDef;
this.operator = args.operator || '';
this.searchTerm = args.searchTerm;
if (this.grid && typeof this.grid.getOptions === 'function') {
this.gridOptions = this.grid.getOptions();
}

// step 1, create the DOM Element of the filter which contain the compound Operator+Input
// and initialize it if searchTerm is filled
this.$filterElm = this.createDomElement();

// step 3, subscribe to the keyup event and run the callback when that happens
// also add/remove "filled" class for styling purposes
this.$filterInputElm.keyup((e: any) => {
this.onTriggerEvent(e);
});
this.$selectOperatorElm.change((e: any) => {
this.onTriggerEvent(e);
});
}
}

/**
* Clear the filter value
*/
clear(triggerFilterKeyup = true) {
if (this.flatInstance && this.$selectOperatorElm) {
this.$selectOperatorElm.val(0);
this.flatInstance.clear();
}
}

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

/**
* Set value(s) on the DOM element
*/
setValues(values: SearchTerm) {
if (values) {
this.flatInstance.setDate(values);
}
}

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

private buildDatePickerInput(searchTerm: SearchTerm) {
const inputFormat = mapFlatpickrDateFormatWithFieldType(this.columnDef.type || FieldType.dateIso);
const outputFormat = mapFlatpickrDateFormatWithFieldType(this.columnDef.outputType || this.columnDef.type || FieldType.dateUtc);
let currentLocale = this.getCurrentLocale(this.columnDef, this.gridOptions) || '';
if (currentLocale.length > 2) {
currentLocale = currentLocale.substring(0, 2);
}

const pickerOptions: any = {
defaultDate: searchTerm || '',
altInput: true,
altFormat: outputFormat,
dateFormat: inputFormat,
wrap: true,
closeOnSelect: true,
locale: (currentLocale !== 'en') ? this.loadFlatpickrLocale(currentLocale) : 'en',
onChange: (selectedDates: any[] | any, dateStr: string, instance: any) => {
this._currentValue = dateStr;

// when using the time picker, we can simulate a keyup event to avoid multiple backend request
// since backend request are only executed after user start typing, changing the time should be treated the same way
if (pickerOptions.enableTime) {
this.onTriggerEvent(new CustomEvent('keyup'));
} else {
this.onTriggerEvent(undefined);
}
}
};

// add the time picker when format is UTC (Z) or has the 'h' (meaning hours)
if (outputFormat && (outputFormat === 'Z' || outputFormat.toLowerCase().includes('h'))) {
pickerOptions.enableTime = true;
}

const placeholder = (this.gridOptions) ? (this.gridOptions.defaultFilterPlaceholder || '') : '';
const $filterInputElm: any = $(`<div class=flatpickr><input type="text" class="form-control" data-input placeholder="${placeholder}"></div>`);
this.flatInstance = (flatpickr && $filterInputElm[0] && typeof $filterInputElm[0].flatpickr === 'function') ? $filterInputElm[0].flatpickr(pickerOptions) : null;
return $filterInputElm;
}

private buildSelectOperatorHtmlString() {
const optionValues = this.getOptionValues();
let optionValueString = '';
optionValues.forEach((option) => {
optionValueString += `<option value="${option.operator}" title="${option.description}">${option.operator}</option>`;
});

return `<select class="form-control">${optionValueString}</select>`;
}

private getOptionValues(): { operator: OperatorString, description: string }[] {
return [
{ operator: '', description: '' },
{ operator: '=', description: '' },
{ operator: '<', description: '' },
{ operator: '<=', description: '' },
{ operator: '>', description: '' },
{ operator: '>=', description: '' },
{ operator: '<>', description: '' }
];
}

/**
* Create the DOM element
*/
private createDomElement() {
const $headerElm = this.grid.getHeaderRowColumn(this.columnDef.id);
$($headerElm).empty();

const searchTerm = (this.searchTerm || '') as string;
if (searchTerm) {
this._currentValue = searchTerm;
}

// create the DOM Select dropdown for the Operator
this.$selectOperatorElm = $(this.buildSelectOperatorHtmlString());
this.$filterInputElm = this.buildDatePickerInput(searchTerm);
const $filterContainerElm = $(`<div class="form-group search-filter"></div>`);
const $containerInputGroup = $(`<div class="input-group flatpickr"></div>`);
const $operatorInputGroupAddon = $(`<div class="input-group-addon operator"></div>`);

/* the DOM element final structure will be
<div class="input-group">
<div class="input-group-addon operator">
<select class="form-control"></select>
</div>
<div class=flatpickr>
<input type="text" class="form-control" data-input>
</div>
</div>
*/
$operatorInputGroupAddon.append(this.$selectOperatorElm);
$containerInputGroup.append($operatorInputGroupAddon);
$containerInputGroup.append(this.$filterInputElm);

// create the DOM element & add an ID and filter class
$filterContainerElm.append($containerInputGroup);
$filterContainerElm.attr('id', `filter-${this.columnDef.id}`);
this.$filterInputElm.data('columnId', this.columnDef.id);

if (this.operator) {
this.$selectOperatorElm.val(this.operator);
}

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

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

return $filterContainerElm;
}

private getCurrentLocale(columnDef: Column, gridOptions: GridOption) {
const params = gridOptions.params || columnDef.params || {};
if (params.i18n && params.i18n instanceof I18N) {
return params.i18n.getLocale();
}

return 'en';
}

private loadFlatpickrLocale(locale: string) {
// change locale if needed, Flatpickr reference: https://chmln.github.io/flatpickr/localization/
if (locale !== 'en') {
const localeDefault: any = require(`flatpickr/dist/l10n/${locale}.js`).default;
return (localeDefault && localeDefault[locale]) ? localeDefault[locale] : 'en';
}
return 'en';
}

private onTriggerEvent(e: Event | undefined) {
const selectedOperator = this.$selectOperatorElm.find('option:selected').text();
(this._currentValue) ? this.$filterElm.addClass('filled') : this.$filterElm.removeClass('filled');
this.callback(e, { columnDef: this.columnDef, searchTerm: this._currentValue, operator: selectedOperator || '=' });
}

private hide() {
if (this.flatInstance && typeof this.flatInstance.close === 'function') {
this.flatInstance.close();
}
}

private show() {
if (this.flatInstance && typeof this.flatInstance.open === 'function') {
this.flatInstance.open();
}
}
}
Loading

0 comments on commit 763e766

Please sign in to comment.