Component-Builder/admin/custom/footable-v3/js/footable.filtering.js

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);