1044 lines
40 KiB
JavaScript
1044 lines
40 KiB
JavaScript
/*
|
|
* FooTable v3 - FooTable is a jQuery plugin that aims to make HTML tables on smaller devices look awesome.
|
|
* @version 3.1.4
|
|
* @link http://fooplugins.com
|
|
* @copyright Steven Usher & Brad Vincent 2015
|
|
* @license Released under the GPLv3 license.
|
|
*/
|
|
(function(F){
|
|
F.Filter = F.Class.extend(/** @lends FooTable.Filter */{
|
|
/**
|
|
* The filter object contains the query to filter by and the columns to apply it to.
|
|
* @constructs
|
|
* @extends FooTable.Class
|
|
* @param {string} name - The name for the filter.
|
|
* @param {(string|FooTable.Query)} query - The query for the filter.
|
|
* @param {Array.<FooTable.Column>} columns - The columns to apply the query to.
|
|
* @param {string} [space="AND"] - How the query treats space chars.
|
|
* @param {boolean} [connectors=true] - Whether or not to replace phrase connectors (+.-_) with spaces.
|
|
* @param {boolean} [ignoreCase=true] - Whether or not ignore case when matching.
|
|
* @param {boolean} [hidden=true] - Whether or not this is a hidden filter.
|
|
* @returns {FooTable.Filter}
|
|
*/
|
|
construct: function(name, query, columns, space, connectors, ignoreCase, hidden){
|
|
/**
|
|
* The name of the filter.
|
|
* @instance
|
|
* @type {string}
|
|
*/
|
|
this.name = name;
|
|
/**
|
|
* A string specifying how the filter treats space characters. Can be either "OR" or "AND".
|
|
* @instance
|
|
* @type {string}
|
|
*/
|
|
this.space = F.is.string(space) && (space == 'OR' || space == 'AND') ? space : 'AND';
|
|
/**
|
|
* Whether or not to replace phrase connectors (+.-_) with spaces before executing the query.
|
|
* @instance
|
|
* @type {boolean}
|
|
*/
|
|
this.connectors = F.is.boolean(connectors) ? connectors : true;
|
|
/**
|
|
* Whether or not ignore case when matching.
|
|
* @instance
|
|
* @type {boolean}
|
|
*/
|
|
this.ignoreCase = F.is.boolean(ignoreCase) ? ignoreCase : true;
|
|
/**
|
|
* Whether or not this is a hidden filter.
|
|
* @instance
|
|
* @type {boolean}
|
|
*/
|
|
this.hidden = F.is.boolean(hidden) ? hidden : false;
|
|
/**
|
|
* The query for the filter.
|
|
* @instance
|
|
* @type {(string|FooTable.Query)}
|
|
*/
|
|
this.query = query instanceof F.Query ? query : new F.Query(query, this.space, this.connectors, this.ignoreCase);
|
|
/**
|
|
* The columns to apply the query to.
|
|
* @instance
|
|
* @type {Array.<FooTable.Column>}
|
|
*/
|
|
this.columns = columns;
|
|
},
|
|
/**
|
|
* Checks if the current filter matches the supplied string.
|
|
* If the current query property is a string it will be auto converted to a {@link FooTable.Query} object to perform the match.
|
|
* @instance
|
|
* @param {string} str - The string to check.
|
|
* @returns {boolean}
|
|
*/
|
|
match: function(str){
|
|
if (!F.is.string(str)) return false;
|
|
if (F.is.string(this.query)){
|
|
this.query = new F.Query(this.query, this.space, this.connectors, this.ignoreCase);
|
|
}
|
|
return this.query instanceof F.Query ? this.query.match(str) : false;
|
|
},
|
|
/**
|
|
* Checks if the current filter matches the supplied {@link FooTable.Row}.
|
|
* @instance
|
|
* @param {FooTable.Row} row - The row to check.
|
|
* @returns {boolean}
|
|
*/
|
|
matchRow: function(row){
|
|
var self = this, text = F.arr.map(row.cells, function(cell){
|
|
return F.arr.contains(self.columns, cell.column) ? cell.filterValue : null;
|
|
}).join(' ');
|
|
return self.match(text);
|
|
}
|
|
});
|
|
|
|
})(FooTable);
|
|
(function ($, F) {
|
|
F.Filtering = F.Component.extend(/** @lends FooTable.Filtering */{
|
|
/**
|
|
* The filtering component adds a search input and column selector dropdown to the table allowing users to filter the using space delimited queries.
|
|
* @constructs
|
|
* @extends FooTable.Component
|
|
* @param {FooTable.Table} table - The parent {@link FooTable.Table} object for the component.
|
|
* @returns {FooTable.Filtering}
|
|
*/
|
|
construct: function (table) {
|
|
// call the constructor of the base class
|
|
this._super(table, table.o.filtering.enabled);
|
|
|
|
/* PUBLIC */
|
|
/**
|
|
* The filters to apply to the current {@link FooTable.Rows#array}.
|
|
* @instance
|
|
* @type {Array.<FooTable.Filter>}
|
|
*/
|
|
this.filters = table.o.filtering.filters;
|
|
/**
|
|
* The delay in milliseconds before the query is auto applied after a change.
|
|
* @instance
|
|
* @type {number}
|
|
*/
|
|
this.delay = table.o.filtering.delay;
|
|
/**
|
|
* The minimum number of characters allowed in the search input before it is auto applied.
|
|
* @instance
|
|
* @type {number}
|
|
*/
|
|
this.min = table.o.filtering.min;
|
|
/**
|
|
* Specifies how whitespace in a filter query is handled.
|
|
* @instance
|
|
* @type {string}
|
|
*/
|
|
this.space = table.o.filtering.space;
|
|
/**
|
|
* Whether or not to replace phrase connectors (+.-_) with spaces before executing the query.
|
|
* @instance
|
|
* @type {boolean}
|
|
*/
|
|
this.connectors = table.o.filtering.connectors;
|
|
/**
|
|
* Whether or not ignore case when matching.
|
|
* @instance
|
|
* @type {boolean}
|
|
*/
|
|
this.ignoreCase = table.o.filtering.ignoreCase;
|
|
/**
|
|
* Whether or not search queries are treated as phrases when matching.
|
|
* @instance
|
|
* @type {boolean}
|
|
*/
|
|
this.exactMatch = table.o.filtering.exactMatch;
|
|
/**
|
|
* The placeholder text to display within the search $input.
|
|
* @instance
|
|
* @type {string}
|
|
*/
|
|
this.placeholder = table.o.filtering.placeholder;
|
|
/**
|
|
* The title to display at the top of the search input column select.
|
|
* @type {string}
|
|
*/
|
|
this.dropdownTitle = table.o.filtering.dropdownTitle;
|
|
/**
|
|
* The position of the $search input within the filtering rows cell.
|
|
* @type {string}
|
|
*/
|
|
this.position = table.o.filtering.position;
|
|
/**
|
|
* The jQuery row object that contains all the filtering specific elements.
|
|
* @instance
|
|
* @type {jQuery}
|
|
*/
|
|
this.$row = null;
|
|
/**
|
|
* The jQuery cell object that contains the search input and column selector.
|
|
* @instance
|
|
* @type {jQuery}
|
|
*/
|
|
this.$cell = null;
|
|
/**
|
|
* The jQuery object of the column selector dropdown.
|
|
* @instance
|
|
* @type {jQuery}
|
|
*/
|
|
this.$dropdown = null;
|
|
/**
|
|
* The jQuery object of the search input.
|
|
* @instance
|
|
* @type {jQuery}
|
|
*/
|
|
this.$input = null;
|
|
/**
|
|
* The jQuery object of the search button.
|
|
* @instance
|
|
* @type {jQuery}
|
|
*/
|
|
this.$button = null;
|
|
|
|
/* PRIVATE */
|
|
/**
|
|
* The timeout ID for the filter changed event.
|
|
* @instance
|
|
* @private
|
|
* @type {?number}
|
|
*/
|
|
this._filterTimeout = null;
|
|
/**
|
|
* The regular expression used to check for encapsulating quotations.
|
|
* @instance
|
|
* @private
|
|
* @type {RegExp}
|
|
*/
|
|
this._exactRegExp = /^"(.*?)"$/;
|
|
},
|
|
|
|
/* PROTECTED */
|
|
/**
|
|
* Checks the supplied data and options for the filtering component.
|
|
* @instance
|
|
* @protected
|
|
* @param {object} data - The jQuery data object from the parent table.
|
|
* @fires FooTable.Filtering#"preinit.ft.filtering"
|
|
*/
|
|
preinit: function(data){
|
|
var self = this;
|
|
/**
|
|
* The preinit.ft.filtering event is raised before the UI is created and provides the tables jQuery data object for additional options parsing.
|
|
* Calling preventDefault on this event will disable the component.
|
|
* @event FooTable.Filtering#"preinit.ft.filtering"
|
|
* @param {jQuery.Event} e - The jQuery.Event object for the event.
|
|
* @param {FooTable.Table} ft - The instance of the plugin raising the event.
|
|
* @param {object} data - The jQuery data object of the table raising the event.
|
|
*/
|
|
return self.ft.raise('preinit.ft.filtering').then(function(){
|
|
// first check if filtering is enabled via the class being applied
|
|
if (self.ft.$el.hasClass('footable-filtering'))
|
|
self.enabled = true;
|
|
// then check if the data-filtering-enabled attribute has been set
|
|
self.enabled = F.is.boolean(data.filtering)
|
|
? data.filtering
|
|
: self.enabled;
|
|
|
|
// if filtering is not enabled exit early as we don't need to do anything else
|
|
if (!self.enabled) return;
|
|
|
|
self.space = F.is.string(data.filterSpace)
|
|
? data.filterSpace
|
|
: self.space;
|
|
|
|
self.min = F.is.number(data.filterMin)
|
|
? data.filterMin
|
|
: self.min;
|
|
|
|
self.connectors = F.is.boolean(data.filterConnectors)
|
|
? data.filterConnectors
|
|
: self.connectors;
|
|
|
|
self.ignoreCase = F.is.boolean(data.filterIgnoreCase)
|
|
? data.filterIgnoreCase
|
|
: self.ignoreCase;
|
|
|
|
self.exactMatch = F.is.boolean(data.filterExactMatch)
|
|
? data.filterExactMatch
|
|
: self.exactMatch;
|
|
|
|
self.delay = F.is.number(data.filterDelay)
|
|
? data.filterDelay
|
|
: self.delay;
|
|
|
|
self.placeholder = F.is.string(data.filterPlaceholder)
|
|
? data.filterPlaceholder
|
|
: self.placeholder;
|
|
|
|
self.dropdownTitle = F.is.string(data.filterDropdownTitle)
|
|
? data.filterDropdownTitle
|
|
: self.dropdownTitle;
|
|
|
|
self.filters = F.is.array(data.filterFilters)
|
|
? self.ensure(data.filterFilters)
|
|
: self.ensure(self.filters);
|
|
|
|
if (self.ft.$el.hasClass('footable-filtering-left'))
|
|
self.position = 'left';
|
|
if (self.ft.$el.hasClass('footable-filtering-center'))
|
|
self.position = 'center';
|
|
if (self.ft.$el.hasClass('footable-filtering-right'))
|
|
self.position = 'right';
|
|
|
|
self.position = F.is.string(data.filterPosition)
|
|
? data.filterPosition
|
|
: self.position;
|
|
},function(){
|
|
self.enabled = false;
|
|
});
|
|
},
|
|
/**
|
|
* Initializes the filtering component for the plugin.
|
|
* @instance
|
|
* @protected
|
|
* @fires FooTable.Filtering#"init.ft.filtering"
|
|
*/
|
|
init: function () {
|
|
var self = this;
|
|
/**
|
|
* The init.ft.filtering event is raised before its UI is generated.
|
|
* Calling preventDefault on this event will disable the component.
|
|
* @event FooTable.Filtering#"init.ft.filtering"
|
|
* @param {jQuery.Event} e - The jQuery.Event object for the event.
|
|
* @param {FooTable.Table} ft - The instance of the plugin raising the event.
|
|
*/
|
|
return self.ft.raise('init.ft.filtering').then(function(){
|
|
self.$create();
|
|
}, function(){
|
|
self.enabled = false;
|
|
});
|
|
},
|
|
/**
|
|
* Destroys the filtering component removing any UI from the table.
|
|
* @instance
|
|
* @protected
|
|
* @fires FooTable.Filtering#"destroy.ft.filtering"
|
|
*/
|
|
destroy: function () {
|
|
/**
|
|
* The destroy.ft.filtering event is raised before its UI is removed.
|
|
* Calling preventDefault on this event will prevent the component from being destroyed.
|
|
* @event FooTable.Filtering#"destroy.ft.filtering"
|
|
* @param {jQuery.Event} e - The jQuery.Event object for the event.
|
|
* @param {FooTable.Table} ft - The instance of the plugin raising the event.
|
|
*/
|
|
var self = this;
|
|
return self.ft.raise('destroy.ft.filtering').then(function(){
|
|
self.ft.$el.removeClass('footable-filtering')
|
|
.find('thead > tr.footable-filtering').remove();
|
|
});
|
|
},
|
|
/**
|
|
* Creates the filtering UI from the current options setting the various jQuery properties of this component.
|
|
* @instance
|
|
* @protected
|
|
* @this FooTable.Filtering
|
|
*/
|
|
$create: function () {
|
|
var self = this;
|
|
// generate the cell that actually contains all the UI.
|
|
var $form_grp = $('<div/>', {'class': 'form-group footable-filtering-search'})
|
|
.append($('<label/>', {'class': 'sr-only', text: 'Search'})),
|
|
$input_grp = $('<div/>', {'class': 'input-group'}).appendTo($form_grp),
|
|
$input_grp_btn = $('<div/>', {'class': 'input-group-btn'}),
|
|
$dropdown_toggle = $('<button/>', {type: 'button', 'class': 'btn btn-default dropdown-toggle'})
|
|
.on('click', { self: self }, self._onDropdownToggleClicked)
|
|
.append($('<span/>', {'class': 'caret'})),
|
|
position;
|
|
|
|
switch (self.position){
|
|
case 'left': position = 'footable-filtering-left'; break;
|
|
case 'center': position = 'footable-filtering-center'; break;
|
|
default: position = 'footable-filtering-right'; break;
|
|
}
|
|
self.ft.$el.addClass('footable-filtering').addClass(position);
|
|
|
|
// add it to a row and then populate it with the search input and column selector dropdown.
|
|
self.$row = $('<tr/>', {'class': 'footable-filtering'}).prependTo(self.ft.$el.children('thead'));
|
|
self.$cell = $('<th/>').attr('colspan', self.ft.columns.visibleColspan).appendTo(self.$row);
|
|
self.$form = $('<form/>', {'class': 'form-inline'}).append($form_grp).appendTo(self.$cell);
|
|
|
|
self.$input = $('<input/>', {type: 'text', 'class': 'form-control', placeholder: self.placeholder});
|
|
|
|
self.$button = $('<button/>', {type: 'button', 'class': 'btn btn-primary'})
|
|
.on('click', { self: self }, self._onSearchButtonClicked)
|
|
.append($('<span/>', {'class': 'fooicon fooicon-search'}));
|
|
|
|
self.$dropdown = $('<ul/>', {'class': 'dropdown-menu dropdown-menu-right'});
|
|
if (!F.is.emptyString(self.dropdownTitle)){
|
|
self.$dropdown.append($('<li/>', {'class': 'dropdown-header','text': self.dropdownTitle}));
|
|
}
|
|
self.$dropdown.append(
|
|
F.arr.map(self.ft.columns.array, function (col) {
|
|
return col.filterable ? $('<li/>').append(
|
|
$('<a/>', {'class': 'checkbox'}).append(
|
|
$('<label/>', {text: col.title}).prepend(
|
|
$('<input/>', {type: 'checkbox', checked: true}).data('__FooTableColumn__', col)
|
|
)
|
|
)
|
|
) : null;
|
|
})
|
|
);
|
|
|
|
if (self.delay > 0){
|
|
self.$input.on('keypress keyup paste', { self: self }, self._onSearchInputChanged);
|
|
self.$dropdown.on('click', 'input[type="checkbox"]', {self: self}, self._onSearchColumnClicked);
|
|
}
|
|
|
|
$input_grp_btn.append(self.$button, $dropdown_toggle, self.$dropdown);
|
|
$input_grp.append(self.$input, $input_grp_btn);
|
|
},
|
|
/**
|
|
* Performs the filtering of rows before they are appended to the page.
|
|
* @instance
|
|
* @protected
|
|
*/
|
|
predraw: function(){
|
|
if (F.is.emptyArray(this.filters))
|
|
return;
|
|
|
|
var self = this;
|
|
self.ft.rows.array = $.grep(self.ft.rows.array, function(r){
|
|
return r.filtered(self.filters);
|
|
});
|
|
},
|
|
/**
|
|
* As the rows are drawn by the {@link FooTable.Rows#draw} method this simply updates the colspan for the UI.
|
|
* @instance
|
|
* @protected
|
|
*/
|
|
draw: function(){
|
|
this.$cell.attr('colspan', this.ft.columns.visibleColspan);
|
|
var search = this.find('search');
|
|
if (search instanceof F.Filter){
|
|
var query = search.query.val();
|
|
if (this.exactMatch && this._exactRegExp.test(query)){
|
|
query = query.replace(this._exactRegExp, '$1');
|
|
}
|
|
this.$input.val(query);
|
|
} else {
|
|
this.$input.val(null);
|
|
}
|
|
this.setButton(!F.arr.any(this.filters, function(f){ return !f.hidden; }));
|
|
},
|
|
|
|
/* PUBLIC */
|
|
/**
|
|
* Adds or updates the filter using the supplied name, query and columns.
|
|
* @instance
|
|
* @param {(string|FooTable.Filter|object)} nameOrFilter - The name for the filter or the actual filter object itself.
|
|
* @param {(string|FooTable.Query)} [query] - The query for the filter. This is only optional when the first parameter is a filter object.
|
|
* @param {(Array.<number>|Array.<string>|Array.<FooTable.Column>)} [columns] - The columns to apply the filter to.
|
|
* If not supplied the filter will be applied to all selected columns in the search input dropdown.
|
|
* @param {boolean} [ignoreCase=true] - Whether or not ignore case when matching.
|
|
* @param {boolean} [connectors=true] - Whether or not to replace phrase connectors (+.-_) with spaces.
|
|
* @param {string} [space="AND"] - How the query treats space chars.
|
|
* @param {boolean} [hidden=true] - Whether or not this is a hidden filter.
|
|
*/
|
|
addFilter: function(nameOrFilter, query, columns, ignoreCase, connectors, space, hidden){
|
|
var f = this.createFilter(nameOrFilter, query, columns, ignoreCase, connectors, space, hidden);
|
|
if (f instanceof F.Filter){
|
|
this.removeFilter(f.name);
|
|
this.filters.push(f);
|
|
}
|
|
},
|
|
/**
|
|
* Removes the filter using the supplied name if it exists.
|
|
* @instance
|
|
* @param {string} name - The name of the filter to remove.
|
|
*/
|
|
removeFilter: function(name){
|
|
F.arr.remove(this.filters, function(f){ return f.name == name; });
|
|
},
|
|
/**
|
|
* Performs the required steps to handle filtering including the raising of the {@link FooTable.Filtering#"before.ft.filtering"} and {@link FooTable.Filtering#"after.ft.filtering"} events.
|
|
* @instance
|
|
* @returns {jQuery.Promise}
|
|
* @fires FooTable.Filtering#"before.ft.filtering"
|
|
* @fires FooTable.Filtering#"after.ft.filtering"
|
|
*/
|
|
filter: function(){
|
|
var self = this;
|
|
self.filters = self.ensure(self.filters);
|
|
/**
|
|
* The before.ft.filtering event is raised before a filter is applied and allows listeners to modify the filter or cancel it completely by calling preventDefault on the jQuery.Event object.
|
|
* @event FooTable.Filtering#"before.ft.filtering"
|
|
* @param {jQuery.Event} e - The jQuery.Event object for the event.
|
|
* @param {FooTable.Table} ft - The instance of the plugin raising the event.
|
|
* @param {Array.<FooTable.Filter>} filters - The filters that are about to be applied.
|
|
*/
|
|
return self.ft.raise('before.ft.filtering', [self.filters]).then(function(){
|
|
self.filters = self.ensure(self.filters);
|
|
return self.ft.draw().then(function(){
|
|
/**
|
|
* The after.ft.filtering event is raised after a filter has been applied.
|
|
* @event FooTable.Filtering#"after.ft.filtering"
|
|
* @param {jQuery.Event} e - The jQuery.Event object for the event.
|
|
* @param {FooTable.Table} ft - The instance of the plugin raising the event.
|
|
* @param {FooTable.Filter} filter - The filters that were applied.
|
|
*/
|
|
self.ft.raise('after.ft.filtering', [self.filters]);
|
|
});
|
|
});
|
|
},
|
|
/**
|
|
* Removes the current search filter.
|
|
* @instance
|
|
* @returns {jQuery.Promise}
|
|
* @fires FooTable.Filtering#"before.ft.filtering"
|
|
* @fires FooTable.Filtering#"after.ft.filtering"
|
|
*/
|
|
clear: function(){
|
|
this.filters = F.arr.get(this.filters, function(f){ return f.hidden; });
|
|
return this.filter();
|
|
},
|
|
/**
|
|
* Toggles the button icon between the search and clear icons based on the supplied value.
|
|
* @instance
|
|
* @param {boolean} search - Whether or not to display the search icon.
|
|
*/
|
|
setButton: function(search){
|
|
if (!search){
|
|
this.$button.children('.fooicon').removeClass('fooicon-search').addClass('fooicon-remove');
|
|
} else {
|
|
this.$button.children('.fooicon').removeClass('fooicon-remove').addClass('fooicon-search');
|
|
}
|
|
},
|
|
/**
|
|
* Finds a filter by name.
|
|
* @param {string} name - The name of the filter to find.
|
|
* @returns {(FooTable.Filter|null)}
|
|
*/
|
|
find: function(name){
|
|
return F.arr.first(this.filters, function(f){ return f.name == name; });
|
|
},
|
|
/**
|
|
* Gets an array of {@link FooTable.Column} to apply the search filter to. This also doubles as the default columns for filters which do not specify any columns.
|
|
* @instance
|
|
* @returns {Array.<FooTable.Column>}
|
|
*/
|
|
columns: function(){
|
|
if (F.is.jq(this.$dropdown)){
|
|
// if we have a dropdown containing the column names get the selected columns from there
|
|
return this.$dropdown.find('input:checked').map(function(){
|
|
return $(this).data('__FooTableColumn__');
|
|
}).get();
|
|
} else {
|
|
// otherwise find all columns that are set to be filterable.
|
|
return this.ft.columns.get(function(c){ return c.filterable; });
|
|
}
|
|
},
|
|
/**
|
|
* Takes an array of plain objects containing the filter values or actual {@link FooTable.Filter} objects and ensures that an array of only {@link FooTable.Filter} is returned.
|
|
* If supplied a plain object that object must contain a name, query and columns properties which are used to create a new {@link FooTable.Filter}.
|
|
* @instance
|
|
* @param {({name: string, query: (string|FooTable.Query), columns: (Array.<string>|Array.<number>|Array.<FooTable.Column>)}|Array.<FooTable.Filter>)} filters - The array of filters to check.
|
|
* @returns {Array.<FooTable.Filter>}
|
|
*/
|
|
ensure: function(filters){
|
|
var self = this, parsed = [], filterable = self.columns();
|
|
if (!F.is.emptyArray(filters)){
|
|
F.arr.each(filters, function(f){
|
|
f = self._ensure(f, filterable);
|
|
if (f instanceof F.Filter) parsed.push(f);
|
|
});
|
|
}
|
|
return parsed;
|
|
},
|
|
|
|
/**
|
|
* Creates a new filter using the supplied object or individual parameters to populate it.
|
|
* @instance
|
|
* @param {(string|FooTable.Filter|object)} nameOrObject - The name for the filter or the actual filter object itself.
|
|
* @param {(string|FooTable.Query)} [query] - The query for the filter. This is only optional when the first parameter is a filter object.
|
|
* @param {(Array.<number>|Array.<string>|Array.<FooTable.Column>)} [columns] - The columns to apply the filter to.
|
|
* If not supplied the filter will be applied to all selected columns in the search input dropdown.
|
|
* @param {boolean} [ignoreCase=true] - Whether or not ignore case when matching.
|
|
* @param {boolean} [connectors=true] - Whether or not to replace phrase connectors (+.-_) with spaces.
|
|
* @param {string} [space="AND"] - How the query treats space chars.
|
|
* @param {boolean} [hidden=true] - Whether or not this is a hidden filter.
|
|
* @returns {*}
|
|
*/
|
|
createFilter: function(nameOrObject, query, columns, ignoreCase, connectors, space, hidden){
|
|
if (F.is.string(nameOrObject)){
|
|
nameOrObject = {name: nameOrObject, query: query, columns: columns, ignoreCase: ignoreCase, connectors: connectors, space: space, hidden: hidden};
|
|
}
|
|
return this._ensure(nameOrObject, this.columns());
|
|
},
|
|
|
|
/* PRIVATE */
|
|
_ensure: function(filter, selectedColumns){
|
|
if ((F.is.hash(filter) || filter instanceof F.Filter) && !F.is.emptyString(filter.name) && (!F.is.emptyString(filter.query) || filter.query instanceof F.Query)){
|
|
filter.columns = F.is.emptyArray(filter.columns) ? selectedColumns : this.ft.columns.ensure(filter.columns);
|
|
filter.ignoreCase = F.is.boolean(filter.ignoreCase) ? filter.ignoreCase : this.ignoreCase;
|
|
filter.connectors = F.is.boolean(filter.connectors) ? filter.connectors : this.connectors;
|
|
filter.hidden = F.is.boolean(filter.hidden) ? filter.hidden : false;
|
|
filter.space = F.is.string(filter.space) && (filter.space === 'AND' || filter.space === 'OR') ? filter.space : this.space;
|
|
filter.query = F.is.string(filter.query) ? new F.Query(filter.query, filter.space, filter.connectors, filter.ignoreCase) : filter.query;
|
|
return (filter instanceof F.Filter)
|
|
? filter
|
|
: new F.Filter(filter.name, filter.query, filter.columns, filter.space, filter.connectors, filter.ignoreCase, filter.hidden);
|
|
}
|
|
return null;
|
|
},
|
|
/**
|
|
* Handles the change event for the {@link FooTable.Filtering#$input}.
|
|
* @instance
|
|
* @private
|
|
* @param {jQuery.Event} e - The event object for the event.
|
|
*/
|
|
_onSearchInputChanged: function (e) {
|
|
var self = e.data.self;
|
|
var alpha = e.type == 'keypress' && !F.is.emptyString(String.fromCharCode(e.charCode)),
|
|
ctrl = e.type == 'keyup' && (e.which == 8 || e.which == 46),
|
|
paste = e.type == 'paste'; // backspace & delete
|
|
|
|
// if alphanumeric characters or specific control characters
|
|
if(alpha || ctrl || paste) {
|
|
if (e.which == 13) e.preventDefault();
|
|
if (self._filterTimeout != null) clearTimeout(self._filterTimeout);
|
|
self._filterTimeout = setTimeout(function(){
|
|
self._filterTimeout = null;
|
|
var query = self.$input.val();
|
|
if (query.length >= self.min){
|
|
if (self.exactMatch && !self._exactRegExp.test(query)){
|
|
query = '"' + query + '"';
|
|
}
|
|
self.addFilter('search', query);
|
|
self.filter();
|
|
} else if (F.is.emptyString(query)){
|
|
self.clear();
|
|
}
|
|
}, self.delay);
|
|
}
|
|
},
|
|
/**
|
|
* Handles the click event for the {@link FooTable.Filtering#$button}.
|
|
* @instance
|
|
* @private
|
|
* @param {jQuery.Event} e - The event object for the event.
|
|
*/
|
|
_onSearchButtonClicked: function (e) {
|
|
e.preventDefault();
|
|
var self = e.data.self;
|
|
if (self._filterTimeout != null) clearTimeout(self._filterTimeout);
|
|
var $icon = self.$button.children('.fooicon');
|
|
if ($icon.hasClass('fooicon-remove')) self.clear();
|
|
else {
|
|
var query = self.$input.val();
|
|
if (query.length >= self.min){
|
|
if (self.exactMatch && !self._exactRegExp.test(query)){
|
|
query = '"' + query + '"';
|
|
}
|
|
self.addFilter('search', query);
|
|
self.filter();
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Handles the click event for the column checkboxes in the {@link FooTable.Filtering#$dropdown}.
|
|
* @instance
|
|
* @private
|
|
* @param {jQuery.Event} e - The event object for the event.
|
|
*/
|
|
_onSearchColumnClicked: function (e) {
|
|
var self = e.data.self;
|
|
if (self._filterTimeout != null) clearTimeout(self._filterTimeout);
|
|
self._filterTimeout = setTimeout(function(){
|
|
self._filterTimeout = null;
|
|
var $icon = self.$button.children('.fooicon');
|
|
if ($icon.hasClass('fooicon-remove')){
|
|
$icon.removeClass('fooicon-remove').addClass('fooicon-search');
|
|
self.addFilter('search', self.$input.val());
|
|
self.filter();
|
|
}
|
|
}, self.delay);
|
|
},
|
|
/**
|
|
* Handles the click event for the {@link FooTable.Filtering#$dropdown} toggle.
|
|
* @instance
|
|
* @private
|
|
* @param {jQuery.Event} e - The event object for the event.
|
|
*/
|
|
_onDropdownToggleClicked: function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var self = e.data.self;
|
|
self.$dropdown.parent().toggleClass('open');
|
|
if (self.$dropdown.parent().hasClass('open')) $(document).on('click.footable', { self: self }, self._onDocumentClicked);
|
|
else $(document).off('click.footable', self._onDocumentClicked);
|
|
},
|
|
/**
|
|
* Checks all click events when the dropdown is visible and closes the menu if the target is not the dropdown.
|
|
* @instance
|
|
* @private
|
|
* @param {jQuery.Event} e - The event object for the event.
|
|
*/
|
|
_onDocumentClicked: function(e){
|
|
if ($(e.target).closest('.dropdown-menu').length == 0){
|
|
e.preventDefault();
|
|
var self = e.data.self;
|
|
self.$dropdown.parent().removeClass('open');
|
|
$(document).off('click.footable', self._onDocumentClicked);
|
|
}
|
|
}
|
|
});
|
|
|
|
F.components.register('filtering', F.Filtering, 500);
|
|
|
|
})(jQuery, FooTable);
|
|
|
|
(function(F){
|
|
F.Query = F.Class.extend(/** @lends FooTable.Query */{
|
|
/**
|
|
* The query object is used to parse and test the filtering component's queries
|
|
* @constructs
|
|
* @extends FooTable.Class
|
|
* @param {string} query - The string value of the query.
|
|
* @param {string} [space="AND"] - How the query treats whitespace.
|
|
* @param {boolean} [connectors=true] - Whether or not to replace phrase connectors (+.-_) with spaces.
|
|
* @param {boolean} [ignoreCase=true] - Whether or not ignore case when matching.
|
|
* @returns {FooTable.Query}
|
|
*/
|
|
construct: function(query, space, connectors, ignoreCase){
|
|
/* PRIVATE */
|
|
/**
|
|
* Holds the previous value of the query and is used internally in the {@link FooTable.Query#val} method.
|
|
* @type {string}
|
|
* @private
|
|
*/
|
|
this._original = null;
|
|
/**
|
|
* Holds the value for the query. Access to this variable is provided through the {@link FooTable.Query#val} method.
|
|
* @type {string}
|
|
* @private
|
|
*/
|
|
this._value = null;
|
|
/* PUBLIC */
|
|
/**
|
|
* A string specifying how the query treats whitespace. Can be either "OR" or "AND".
|
|
* @type {string}
|
|
*/
|
|
this.space = F.is.string(space) && (space == 'OR' || space == 'AND') ? space : 'AND';
|
|
/**
|
|
* Whether or not to replace phrase connectors (+.-_) with spaces before executing the query.
|
|
* @instance
|
|
* @type {boolean}
|
|
*/
|
|
this.connectors = F.is.boolean(connectors) ? connectors : true;
|
|
/**
|
|
* Whether or not ignore case when matching.
|
|
* @instance
|
|
* @type {boolean}
|
|
*/
|
|
this.ignoreCase = F.is.boolean(ignoreCase) ? ignoreCase : true;
|
|
/**
|
|
* The left side of the query if one exists. OR takes precedence over AND.
|
|
* @type {FooTable.Query}
|
|
* @example <caption>The below shows what is meant by the "left" side of a query</caption>
|
|
* query = "Dave AND Mary" - "Dave" is the left side of the query.
|
|
* query = "Dave AND Mary OR John" - "Dave and Mary" is the left side of the query.
|
|
*/
|
|
this.left = null;
|
|
/**
|
|
* The right side of the query if one exists. OR takes precedence over AND.
|
|
* @type {FooTable.Query}
|
|
* @example <caption>The below shows what is meant by the "right" side of a query</caption>
|
|
* query = "Dave AND Mary" - "Mary" is the right side of the query.
|
|
* query = "Dave AND Mary OR John" - "John" is the right side of the query.
|
|
*/
|
|
this.right = null;
|
|
/**
|
|
* The parsed parts of the query. This contains the information used to actually perform a match against a string.
|
|
* @type {Array}
|
|
*/
|
|
this.parts = [];
|
|
/**
|
|
* The type of operand to apply to the results of the individual parts of the query.
|
|
* @type {string}
|
|
*/
|
|
this.operator = null;
|
|
this.val(query);
|
|
},
|
|
/**
|
|
* Gets or sets the value for the query. During set the value is parsed setting all properties as required.
|
|
* @param {string} [value] - If supplied the value to set for this query.
|
|
* @returns {(string|undefined)}
|
|
*/
|
|
val: function(value){
|
|
// get
|
|
if (F.is.emptyString(value)) return this._value;
|
|
|
|
// set
|
|
if (F.is.emptyString(this._original)) this._original = value;
|
|
else if (this._original == value) return;
|
|
|
|
this._value = value;
|
|
this._parse();
|
|
},
|
|
/**
|
|
* Tests the supplied string against the query.
|
|
* @param {string} str - The string to test.
|
|
* @returns {boolean}
|
|
*/
|
|
match: function(str){
|
|
if (F.is.emptyString(this.operator) || this.operator === 'OR')
|
|
return this._left(str, false) || this._match(str, false) || this._right(str, false);
|
|
if (this.operator === 'AND')
|
|
return this._left(str, true) && this._match(str, true) && this._right(str, true);
|
|
},
|
|
/**
|
|
* Matches this queries parts array against the supplied string.
|
|
* @param {string} str - The string to test.
|
|
* @param {boolean} def - The default value to return based on the operand.
|
|
* @returns {boolean}
|
|
* @private
|
|
*/
|
|
_match: function(str, def){
|
|
var self = this, result = false, empty = F.is.emptyString(str);
|
|
if (F.is.emptyArray(self.parts) && self.left instanceof F.Query) return def;
|
|
if (F.is.emptyArray(self.parts)) return result;
|
|
if (self.space === 'OR'){
|
|
// with OR we give the str every part to test and if any match it is a success, we do exit early if a negated match occurs
|
|
F.arr.each(self.parts, function(p){
|
|
if (p.empty && empty){
|
|
result = true;
|
|
if (p.negate){
|
|
result = false;
|
|
return result;
|
|
}
|
|
} else {
|
|
var match = (p.exact ? F.str.containsExact : F.str.contains)(str, p.query, self.ignoreCase);
|
|
if (match && !p.negate) result = true;
|
|
if (match && p.negate) {
|
|
result = false;
|
|
return result;
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
// otherwise with AND we check until the first failure and then exit
|
|
result = true;
|
|
F.arr.each(self.parts, function(p){
|
|
if (p.empty){
|
|
if ((!empty && !p.negate) || (empty && p.negate)) result = false;
|
|
return result;
|
|
} else {
|
|
var match = (p.exact ? F.str.containsExact : F.str.contains)(str, p.query, self.ignoreCase);
|
|
if ((!match && !p.negate) || (match && p.negate)) result = false;
|
|
return result;
|
|
}
|
|
});
|
|
}
|
|
return result;
|
|
},
|
|
/**
|
|
* Matches the left side of the query if one exists with the supplied string.
|
|
* @param {string} str - The string to test.
|
|
* @param {boolean} def - The default value to return based on the operand.
|
|
* @returns {boolean}
|
|
* @private
|
|
*/
|
|
_left: function(str, def){
|
|
return (this.left instanceof F.Query) ? this.left.match(str) : def;
|
|
},
|
|
/**
|
|
* Matches the right side of the query if one exists with the supplied string.
|
|
* @param {string} str - The string to test.
|
|
* @param {boolean} def - The default value to return based on the operand.
|
|
* @returns {boolean}
|
|
* @private
|
|
*/
|
|
_right: function(str, def){
|
|
return (this.right instanceof F.Query) ? this.right.match(str) : def;
|
|
},
|
|
/**
|
|
* Parses the private {@link FooTable.Query#_value} property and populates the object.
|
|
* @private
|
|
*/
|
|
_parse: function(){
|
|
if (F.is.emptyString(this._value)) return;
|
|
// OR takes precedence so test for it first
|
|
if (/\sOR\s/.test(this._value)){
|
|
// we have an OR so split the value on the first occurrence of OR to get the left and right sides of the statement
|
|
this.operator = 'OR';
|
|
var or = this._value.split(/(?:\sOR\s)(.*)?/);
|
|
this.left = new F.Query(or[0], this.space, this.connectors, this.ignoreCase);
|
|
this.right = new F.Query(or[1], this.space, this.connectors, this.ignoreCase);
|
|
} else if (/\sAND\s/.test(this._value)) {
|
|
// there are no more OR's so start with AND
|
|
this.operator = 'AND';
|
|
var and = this._value.split(/(?:\sAND\s)(.*)?/);
|
|
this.left = new F.Query(and[0], this.space, this.connectors, this.ignoreCase);
|
|
this.right = new F.Query(and[1], this.space, this.connectors, this.ignoreCase);
|
|
} else {
|
|
// we have no more statements to parse so set the parts array by parsing each part of the remaining query
|
|
var self = this;
|
|
this.parts = F.arr.map(this._value.match(/(?:[^\s"]+|"[^"]*")+/g), function(str){
|
|
return self._part(str);
|
|
});
|
|
}
|
|
},
|
|
/**
|
|
* Parses a single part of a query into an object to use during matching.
|
|
* @param {string} str - The string representation of the part.
|
|
* @returns {{query: string, negate: boolean, phrase: boolean, exact: boolean}}
|
|
* @private
|
|
*/
|
|
_part: function(str){
|
|
var p = {
|
|
query: str,
|
|
negate: false,
|
|
phrase: false,
|
|
exact: false,
|
|
empty: false
|
|
};
|
|
// support for NEGATE operand - (minus sign). Remove this first so we can get onto phrase checking
|
|
if (F.str.startsWith(p.query, '-')){
|
|
p.query = F.str.from(p.query, '-');
|
|
p.negate = true;
|
|
}
|
|
// support for PHRASES (exact matches)
|
|
if (/^"(.*?)"$/.test(p.query)){ // if surrounded in quotes strip them and nothing else
|
|
p.query = p.query.replace(/^"(.*?)"$/, '$1');
|
|
p.phrase = true;
|
|
p.exact = true;
|
|
} else if (this.connectors && /(?:\w)+?([-_\+\.])(?:\w)+?/.test(p.query)) { // otherwise replace supported phrase connectors (-_+.) with spaces
|
|
p.query = p.query.replace(/(?:\w)+?([-_\+\.])(?:\w)+?/g, function(match, p1){
|
|
return match.replace(p1, ' ');
|
|
});
|
|
p.phrase = true;
|
|
}
|
|
p.empty = p.phrase && F.is.emptyString(p.query);
|
|
return p;
|
|
}
|
|
});
|
|
|
|
})(FooTable);
|
|
(function(F){
|
|
|
|
/**
|
|
* The value used by the filtering component during filter operations. Must be a string and can be set using the data-filter-value attribute on the cell itself.
|
|
* If this is not supplied it is set to the result of the toString method called on the value for the cell. Added by the {@link FooTable.Filtering} component.
|
|
* @type {string}
|
|
* @default null
|
|
*/
|
|
F.Cell.prototype.filterValue = null;
|
|
|
|
// this is used to define the filtering specific properties on cell creation
|
|
F.Cell.prototype.__filtering_define__ = function(valueOrElement){
|
|
this.filterValue = this.column.filterValue.call(this.column, valueOrElement);
|
|
};
|
|
|
|
// this is used to update the filterValue property whenever the cell value is changed
|
|
F.Cell.prototype.__filtering_val__ = function(value){
|
|
if (F.is.defined(value)){
|
|
// set only
|
|
this.filterValue = this.column.filterValue.call(this.column, value);
|
|
}
|
|
};
|
|
|
|
// overrides the public define method and replaces it with our own
|
|
F.Cell.extend('define', function(valueOrElement){
|
|
this._super(valueOrElement);
|
|
this.__filtering_define__(valueOrElement);
|
|
});
|
|
// overrides the public val method and replaces it with our own
|
|
F.Cell.extend('val', function(value){
|
|
var val = this._super(value);
|
|
this.__filtering_val__(value);
|
|
return val;
|
|
});
|
|
})(FooTable);
|
|
(function($, F){
|
|
/**
|
|
* Whether or not the column can be used during filtering. Added by the {@link FooTable.Filtering} component.
|
|
* @type {boolean}
|
|
* @default true
|
|
*/
|
|
F.Column.prototype.filterable = true;
|
|
|
|
/**
|
|
* This is supplied either the cell value or jQuery object to parse. A string value must be returned from this method and will be used during filtering operations.
|
|
* @param {(*|jQuery)} valueOrElement - The value or jQuery cell object.
|
|
* @returns {string}
|
|
* @this FooTable.Column
|
|
*/
|
|
F.Column.prototype.filterValue = function(valueOrElement){
|
|
// if we have an element or a jQuery object use jQuery to get the value
|
|
if (F.is.element(valueOrElement) || F.is.jq(valueOrElement)){
|
|
var data = $(valueOrElement).data('filterValue');
|
|
return F.is.defined(data) ? ''+data : $(valueOrElement).text();
|
|
}
|
|
// if options are supplied with the value
|
|
if (F.is.hash(valueOrElement) && F.is.hash(valueOrElement.options)){
|
|
if (F.is.string(valueOrElement.options.filterValue)) return valueOrElement.options.filterValue;
|
|
if (F.is.defined(valueOrElement.value)) valueOrElement = valueOrElement.value;
|
|
}
|
|
if (F.is.defined(valueOrElement) && valueOrElement != null) return valueOrElement+''; // use the native toString of the value
|
|
return ''; // otherwise we have no value so return an empty string
|
|
};
|
|
|
|
// this is used to define the filtering specific properties on column creation
|
|
F.Column.prototype.__filtering_define__ = function(definition){
|
|
this.filterable = F.is.boolean(definition.filterable) ? definition.filterable : this.filterable;
|
|
this.filterValue = F.checkFnValue(this, definition.filterValue, this.filterValue);
|
|
};
|
|
|
|
// overrides the public define method and replaces it with our own
|
|
F.Column.extend('define', function(definition){
|
|
this._super(definition); // call the base so we don't have to redefine any previously set properties
|
|
this.__filtering_define__(definition); // then call our own
|
|
});
|
|
})(jQuery, FooTable);
|
|
(function(F){
|
|
/**
|
|
* An object containing the filtering options for the plugin. Added by the {@link FooTable.Filtering} component.
|
|
* @type {object}
|
|
* @prop {boolean} enabled=false - Whether or not to allow filtering on the table.
|
|
* @prop {({name: string, query: (string|FooTable.Query), columns: (Array.<string>|Array.<number>|Array.<FooTable.Column>)}|Array.<FooTable.Filter>)} filters - The filters to apply to the current {@link FooTable.Rows#array}.
|
|
* @prop {number} delay=1200 - The delay in milliseconds before the query is auto applied after a change (any value equal to or less than zero will disable this).
|
|
* @prop {number} min=1 - The minimum number of characters allowed in the search input before it is auto applied.
|
|
* @prop {string} space="AND" - Specifies how whitespace in a filter query is handled.
|
|
* @prop {string} placeholder="Search" - The string used as the placeholder for the search input.
|
|
* @prop {string} dropdownTitle=null - The title to display at the top of the search input column select.
|
|
* @prop {string} position="right" - The string used to specify the alignment of the search input.
|
|
* @prop {string} connectors=true - Whether or not to replace phrase connectors (+.-_) with space before executing the query.
|
|
* @prop {boolean} ignoreCase=true - Whether or not ignore case when matching.
|
|
* @prop {boolean} exactMatch=false - Whether or not search queries are treated as phrases when matching.
|
|
*/
|
|
F.Defaults.prototype.filtering = {
|
|
enabled: false,
|
|
filters: [],
|
|
delay: 1200,
|
|
min: 1,
|
|
space: 'AND',
|
|
placeholder: 'Search',
|
|
dropdownTitle: null,
|
|
position: 'right',
|
|
connectors: true,
|
|
ignoreCase: true,
|
|
exactMatch: false
|
|
};
|
|
})(FooTable);
|
|
(function(F){
|
|
/**
|
|
* Checks if the row is filtered using the supplied filters.
|
|
* @this FooTable.Row
|
|
* @param {Array.<FooTable.Filter>} filters - The filters to apply.
|
|
* @returns {boolean}
|
|
*/
|
|
F.Row.prototype.filtered = function(filters){
|
|
var result = true, self = this;
|
|
F.arr.each(filters, function(f){
|
|
if ((result = f.matchRow(self)) == false) return false;
|
|
});
|
|
return result;
|
|
};
|
|
})(FooTable); |