2 * File: jquery.dataTables.columnFilter.js
4 * Author: Jovan Popovic
6 * Copyright 2011-2012 Jovan Popovic, all rights reserved.
8 * This source file is free software, under either the GPL v2 license or a
9 * BSD style license, as supplied with this software.
11 * This source file is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 * or FITNESS FOR A PARTICULAR PURPOSE.
16 * @sPlaceHolder String Place where inline filtering function should be placed ("tfoot", "thead:before", "thead:after"). Default is "tfoot"
17 * @sRangeSeparator String Separator that will be used when range values are sent to the server-side. Default value is "~".
18 * @sRangeFormat string Default format of the From ... to ... range inputs. Default is From {from} to {to}
19 * @aoColumns Array Array of the filter settings that will be applied on the columns
24 $.fn.columnFilter = function (options) {
26 var asInitVals, i, label, th;
28 //var sTableId = "table";
29 var sRangeFormat = "From {from} to {to}";
30 //Array of the functions that will override sSearch_ parameters
31 var afnSearch_ = new Array();
32 var aiCustomSearch_Indexes = new Array();
34 var oFunctionTimeout = null;
36 var fnOnFiltered = function () { };
38 function _fnGetColumnValues(oSettings, iColumn, bUnique, bFiltered, bIgnoreEmpty) {
40 ///Return values in the column
42 ///<param name="oSettings" type="Object">DataTables settings</param>
43 ///<param name="iColumn" type="int">Id of the column</param>
44 ///<param name="bUnique" type="bool">Return only distinct values</param>
45 ///<param name="bFiltered" type="bool">Return values only from the filtered rows</param>
46 ///<param name="bIgnoreEmpty" type="bool">Ignore empty cells</param>
48 // check that we have a column id
49 if (typeof iColumn == "undefined") return new Array();
51 // by default we only wany unique data
52 if (typeof bUnique == "undefined") bUnique = true;
54 // by default we do want to only look at filtered data
55 if (typeof bFiltered == "undefined") bFiltered = true;
57 // by default we do not wany to include empty values
58 if (typeof bIgnoreEmpty == "undefined") bIgnoreEmpty = true;
60 // list of rows which we're going to loop through
63 // use only filtered rows
64 if (bFiltered == true) aiRows = oSettings.aiDisplay;
66 else aiRows = oSettings.aiDisplayMaster; // all row numbers
69 var asResultData = new Array();
71 for (var i = 0, c = aiRows.length; i < c; i++) {
73 var aData = oTable.fnGetData(iRow);
74 var sValue = aData[iColumn];
76 // ignore empty values?
77 if (bIgnoreEmpty == true && sValue.length == 0) continue;
79 // ignore unique values?
80 else if (bUnique == true && jQuery.inArray(sValue, asResultData) > -1) continue;
82 // else push the value onto the result data array
83 else asResultData.push(sValue);
86 return asResultData.sort();
89 function _fnColumnIndex(iColumnIndex) {
90 if (properties.bUseColVis)
93 return oTable.fnSettings().oApi._fnVisibleToColumnIndex(oTable.fnSettings(), iColumnIndex);
94 //return iColumnIndex;
95 //return oTable.fnSettings().oApi._fnColumnIndexToVisible(oTable.fnSettings(), iColumnIndex);
98 function fnCreateInput(oTable, regex, smart, bIsNumber, iFilterLength, iMaxLenght) {
99 var sCSSClass = "text_filter";
101 sCSSClass = "number_filter";
103 label = label.replace(/(^\s*)|(\s*$)/g, "");
104 var currentFilter = oTable.fnSettings().aoPreSearchCols[i].sSearch;
105 var search_init = 'search_init ';
106 var inputvalue = label;
107 if (currentFilter != '' && currentFilter != '^') {
108 if (bIsNumber && currentFilter.charAt(0) == '^')
109 inputvalue = currentFilter.substr(1); //ignore trailing ^
111 inputvalue = currentFilter;
115 var input = $('<input type="text" class="' + search_init + sCSSClass + '" value="' + inputvalue + '"/>');
116 if (iMaxLenght != undefined && iMaxLenght != -1) {
117 input.attr('maxlength', iMaxLenght);
121 th.wrapInner('<span class="filter_column filter_number" />');
123 th.wrapInner('<span class="filter_column filter_text" />');
125 asInitVals[i] = label;
128 if (bIsNumber && !oTable.fnSettings().oFeatures.bServerSide) {
129 input.keyup(function () {
130 /* Filter on the column all numbers that starts with the entered value */
131 oTable.fnFilter('^' + this.value, _fnColumnIndex(index), true, false); //Issue 37
135 input.keyup(function () {
136 if (oTable.fnSettings().oFeatures.bServerSide && iFilterLength != 0) {
137 //If filter length is set in the server-side processing mode
138 //Check has the user entered at least iFilterLength new characters
140 var currentFilter = oTable.fnSettings().aoPreSearchCols[index].sSearch;
141 var iLastFilterLength = $(this).data("dt-iLastFilterLength");
142 if (typeof iLastFilterLength == "undefined")
143 iLastFilterLength = 0;
144 var iCurrentFilterLength = this.value.length;
145 if (Math.abs(iCurrentFilterLength - iLastFilterLength) < iFilterLength
146 //&& currentFilter.length == 0 //Why this?
148 //Cancel the filtering
152 //Remember the current filter length
153 $(this).data("dt-iLastFilterLength", iCurrentFilterLength);
156 /* Filter on the column (the index) of this element */
157 oTable.fnFilter(this.value, _fnColumnIndex(index), regex, smart); //Issue 37
162 input.focus(function () {
163 if ($(this).hasClass("search_init")) {
164 $(this).removeClass("search_init");
168 input.blur(function () {
169 if (this.value == "") {
170 $(this).addClass("search_init");
171 this.value = asInitVals[index];
176 function fnCreateRangeInput(oTable) {
178 //var currentFilter = oTable.fnSettings().aoPreSearchCols[i].sSearch;
179 th.html(_fnRangeLabelPart(0));
180 var sFromId = oTable.attr("id") + '_range_from_' + i;
181 var from = $('<input type="text" class="number_range_filter" id="' + sFromId + '" rel="' + i + '"/>');
183 th.append(_fnRangeLabelPart(1));
184 var sToId = oTable.attr("id") + '_range_to_' + i;
185 var to = $('<input type="text" class="number_range_filter" id="' + sToId + '" rel="' + i + '"/>');
187 th.append(_fnRangeLabelPart(2));
188 th.wrapInner('<span class="filter_column filter_number_range" />');
190 aiCustomSearch_Indexes.push(i);
194 //------------start range filtering function
197 /* Custom filtering function which will filter data in column four between two values
198 * Author: Allan Jardine, Modified by Jovan Popovic
200 //$.fn.dataTableExt.afnFiltering.push(
201 oTable.dataTableExt.afnFiltering.push(
202 function (oSettings, aData, iDataIndex) {
203 if (oTable.attr("id") != oSettings.sTableId)
205 // Try to handle missing nodes more gracefully
206 if (document.getElementById(sFromId) == null)
208 var iMin = document.getElementById(sFromId).value * 1;
209 var iMax = document.getElementById(sToId).value * 1;
210 var iValue = aData[_fnColumnIndex(index)] == "-" ? 0 : aData[_fnColumnIndex(index)] * 1;
211 if (iMin == "" && iMax == "") {
214 else if (iMin == "" && iValue <= iMax) {
217 else if (iMin <= iValue && "" == iMax) {
220 else if (iMin <= iValue && iValue <= iMax) {
226 //------------end range filtering function
230 $('#' + sFromId + ',#' + sToId, th).keyup(function () {
232 var iMin = document.getElementById(sFromId).value * 1;
233 var iMax = document.getElementById(sToId).value * 1;
234 if (iMin != 0 && iMax != 0 && iMin > iMax)
245 function fnCreateDateRangeInput(oTable) {
247 var aoFragments = sRangeFormat.split(/[}{]/);
250 //th.html(_fnRangeLabelPart(0));
251 var sFromId = oTable.attr("id") + '_range_from_' + i;
252 var from = $('<input type="text" class="date_range_filter" id="' + sFromId + '" rel="' + i + '"/>');
255 //th.append(_fnRangeLabelPart(1));
256 var sToId = oTable.attr("id") + '_range_to_' + i;
257 var to = $('<input type="text" class="date_range_filter" id="' + sToId + '" rel="' + i + '"/>');
259 //th.append(_fnRangeLabelPart(2));
261 for (ti = 0; ti < aoFragments.length; ti++) {
263 if (aoFragments[ti] == properties.sDateFromToken) {
266 if (aoFragments[ti] == properties.sDateToToken) {
269 th.append(aoFragments[ti]);
277 th.wrapInner('<span class="filter_column filter_date_range" />');
280 aiCustomSearch_Indexes.push(i);
283 //------------start date range filtering function
285 //$.fn.dataTableExt.afnFiltering.push(
286 oTable.dataTableExt.afnFiltering.push(
287 function (oSettings, aData, iDataIndex) {
288 if (oTable.attr("id") != oSettings.sTableId)
291 var dStartDate = from.datepicker("getDate");
293 var dEndDate = to.datepicker("getDate");
295 if (dStartDate == null && dEndDate == null) {
299 var dCellDate = null;
301 if (aData[_fnColumnIndex(index)] == null || aData[_fnColumnIndex(index)] == "")
303 dCellDate = $.datepicker.parseDate($.datepicker.regional[""].dateFormat, aData[_fnColumnIndex(index)]);
307 if (dCellDate == null)
311 if (dStartDate == null && dCellDate <= dEndDate) {
314 else if (dStartDate <= dCellDate && dEndDate == null) {
317 else if (dStartDate <= dCellDate && dCellDate <= dEndDate) {
323 //------------end date range filtering function
325 $('#' + sFromId + ',#' + sToId, th).change(function () {
333 function fnCreateColumnSelect(oTable, aData, iColumn, nTh, sLabel, bRegex, oSelected) {
335 aData = _fnGetColumnValues(oTable.fnSettings(), iColumn, true, false, true);
337 var currentFilter = oTable.fnSettings().aoPreSearchCols[i].sSearch;
338 if (currentFilter == null || currentFilter == "")//Issue 81
339 currentFilter = oSelected;
341 var r = '<select class="search_init select_filter"><option value="" class="search_init">' + sLabel + '</option>';
343 var iLen = aData.length;
344 for (j = 0; j < iLen; j++) {
345 if (typeof (aData[j]) != 'object') {
347 if (escape(aData[j]) == currentFilter
348 || escape(aData[j]) == escape(currentFilter)
350 selected = 'selected '
351 r += '<option ' + selected + ' value="' + escape(aData[j]) + '">' + aData[j] + '</option>';
356 //Do not escape values if they are explicitely set to avoid escaping special characters in the regexp
357 if (aData[j].value == currentFilter) selected = 'selected ';
358 r += '<option ' + selected + 'value="' + aData[j].value + '">' + aData[j].label + '</option>';
360 if (escape(aData[j].value) == currentFilter) selected = 'selected ';
361 r += '<option ' + selected + 'value="' + escape(aData[j].value) + '">' + aData[j].label + '</option>';
366 var select = $(r + '</select>');
368 nTh.wrapInner('<span class="filter_column filter_select" />');
369 select.change(function () {
370 //var val = $(this).val();
371 if ($(this).val() != "") {
372 $(this).removeClass("search_init");
374 $(this).addClass("search_init");
377 oTable.fnFilter($(this).val(), iColumn, bRegex); //Issue 41
379 oTable.fnFilter(unescape($(this).val()), iColumn); //Issue 25
382 if (currentFilter != null && currentFilter != "")//Issue 81
383 oTable.fnFilter(unescape(currentFilter), iColumn);
386 function fnCreateSelect(oTable, aData, bRegex, oSelected) {
387 var oSettings = oTable.fnSettings();
388 if (aData == null && oSettings.sAjaxSource != "" && !oSettings.oFeatures.bServerSide) {
389 // Add a function to the draw callback, which will check for the Ajax data having
390 // been loaded. Use a closure for the individual column elements that are used to
391 // built the column filter, since 'i' and 'th' (etc) are locally "global".
392 oSettings.aoDrawCallback.push({
393 "fn": (function (iColumn, nTh, sLabel) {
395 // Only rebuild the select on the second draw - i.e. when the Ajax
396 // data has been loaded.
397 if (oSettings.iDraw == 2 && oSettings.sAjaxSource != null && oSettings.sAjaxSource != "" && !oSettings.oFeatures.bServerSide) {
398 return fnCreateColumnSelect(oTable, null, _fnColumnIndex(iColumn), nTh, sLabel, bRegex, oSelected); //Issue 37
402 "sName": "column_filter_" + i
405 // Regardless of the Ajax state, build the select on first pass
406 fnCreateColumnSelect(oTable, aData, _fnColumnIndex(i), th, label, bRegex, oSelected); //Issue 37
410 function fnCreateCheckbox(oTable, aData) {
413 aData = _fnGetColumnValues(oTable.fnSettings(), i, true, true, true);
416 var r = '', j, iLen = aData.length;
419 var localLabel = label.replace('%', 'Perc').replace("&", "AND").replace("$", "DOL").replace("£", "STERL").replace("@", "AT").replace(/\s/g, "_");
420 localLabel = localLabel.replace(/[^a-zA-Z 0-9]+/g, '');
423 //button label override
424 var labelBtn = label;
425 if (properties.sFilterButtonText != null || properties.sFilterButtonText != undefined) {
426 labelBtn = properties.sFilterButtonText;
429 var relativeDivWidthToggleSize = 10;
430 var numRow = 12; //numero di checkbox per colonna
431 var numCol = Math.floor(iLen / numRow);
432 if (iLen % numRow > 0) {
436 //count how many column should be generated and split the div size
437 var divWidth = 100 / numCol - 2;
439 var divWidthToggle = relativeDivWidthToggleSize * numCol;
445 var divRowDef = '<div style="float:left; min-width: ' + divWidth + '%; " >';
446 var divClose = '</div>';
448 var uniqueId = oTable.attr("id") + localLabel;
449 var buttonId = "chkBtnOpen" + uniqueId;
450 var checkToggleDiv = uniqueId + "-flt-toggle";
451 r += '<button id="' + buttonId + '" class="checkbox_filter" > ' + labelBtn + '</button>'; //filter button witch open dialog
452 r += '<div id="' + checkToggleDiv + '" '
453 + 'title="' + label + '" '
454 + 'class="toggle-check ui-widget-content ui-corner-all" style="width: ' + (divWidthToggle) + '%; " >'; //dialog div
455 //r+= '<div align="center" style="margin-top: 5px; "> <button id="'+buttonId+'Reset" class="checkbox_filter" > reset </button> </div>'; //reset button and its div
458 for (j = 0; j < iLen; j++) {
460 //if last check close div
461 if (j % numRow == 0 && j != 0) {
462 r += divClose + divRowDef;
466 r += '<input class="search_init checkbox_filter" type="checkbox" id= "' + aData[j] + '" name= "' + localLabel + '" value="' + aData[j] + '" >' + aData[j] + '<br/>';
470 th.wrapInner('<span class="filter_column filter_checkbox" />');
471 //on every checkbox selection
472 checkbox.change(function () {
475 var or = '|'; //var for select checks in 'or' into the regex
476 var resSize = $('input:checkbox[name="' + localLabel + '"]:checked').size();
477 $('input:checkbox[name="' + localLabel + '"]:checked').each(function (index) {
479 //search = search + ' ' + $(this).val();
480 //concatenation for selected checks in or
481 if ((index == 0 && resSize == 1)
482 || (index != 0 && index == resSize - 1)) {
486 search = search.replace(/^\s+|\s+$/g, "");
487 search = search + $(this).val() + or;
492 for (var jj = 0; jj < iLen; jj++) {
494 $('#' + aData[jj]).removeClass("search_init");
496 $('#' + aData[jj]).addClass("search_init");
501 oTable.fnFilter(search, index, true, false);
507 $('#' + buttonId).button();
509 $('#' + checkToggleDiv).dialog({
517 //$('#'+buttonId).removeClass("filter_selected"); //LM remove border if filter selected
518 $('input:checkbox[name="' + localLabel + '"]:checked').each(function (index3) {
519 $(this).attr('checked', false);
520 $(this).addClass("search_init");
522 oTable.fnFilter('', index, true, false);
529 click: function () { $(this).dialog("close"); }
535 $('#' + buttonId).click(function () {
537 $('#' + checkToggleDiv).dialog('open');
538 var target = $(this);
539 $('#' + checkToggleDiv).dialog("widget").position({ my: 'top',
547 var fnOnFilteredCurrent = fnOnFiltered;
549 fnOnFiltered = function () {
550 var target = $('#' + buttonId);
551 $('#' + checkToggleDiv).dialog("widget").position({ my: 'top',
555 fnOnFilteredCurrent();
559 $('#'+buttonId+"Reset").button();
560 $('#'+buttonId+"Reset").click(function(){
561 $('#'+buttonId).removeClass("filter_selected"); //LM remove border if filter selected
562 $('input:checkbox[name="'+localLabel+'"]:checked').each(function(index3) {
563 $(this).attr('checked', false);
564 $(this).addClass("search_init");
566 oTable.fnFilter('', index, true, false);
575 function _fnRangeLabelPart(iPlace) {
578 return sRangeFormat.substring(0, sRangeFormat.indexOf("{from}"));
580 return sRangeFormat.substring(sRangeFormat.indexOf("{from}") + 6, sRangeFormat.indexOf("{to}"));
582 return sRangeFormat.substring(sRangeFormat.indexOf("{to}") + 4);
592 sPlaceHolder: "foot",
593 sRangeSeparator: "~",
594 iFilteringDelay: 500,
596 sRangeFormat: "From {from} to {to}",
597 sDateFromToken: "from",
601 var properties = $.extend(defaults, options);
603 return this.each(function () {
605 if (!oTable.fnSettings().oFeatures.bFilter)
607 asInitVals = new Array();
609 var aoFilterCells = oTable.fnSettings().aoFooter[0];
611 var oHost = oTable.fnSettings().nTFoot; //Before fix for ColVis
612 var sFilterRow = "tr"; //Before fix for ColVis
614 if (properties.sPlaceHolder == "head:after") {
615 var tr = $("tr:first", oTable.fnSettings().nTHead).detach();
616 //tr.appendTo($(oTable.fnSettings().nTHead));
617 if (oTable.fnSettings().bSortCellsTop) {
618 tr.prependTo($(oTable.fnSettings().nTHead));
619 //tr.appendTo($("thead", oTable));
620 aoFilterCells = oTable.fnSettings().aoHeader[1];
623 tr.appendTo($(oTable.fnSettings().nTHead));
624 //tr.prependTo($("thead", oTable));
625 aoFilterCells = oTable.fnSettings().aoHeader[0];
628 sFilterRow = "tr:last";
629 oHost = oTable.fnSettings().nTHead;
631 } else if (properties.sPlaceHolder == "head:before") {
633 if (oTable.fnSettings().bSortCellsTop) {
634 var tr = $("tr:first", oTable.fnSettings().nTHead).detach();
635 tr.appendTo($(oTable.fnSettings().nTHead));
636 aoFilterCells = oTable.fnSettings().aoHeader[1];
638 aoFilterCells = oTable.fnSettings().aoHeader[0];
641 //tr.prependTo($("thead", oTable));
642 sFilterRow = "tr:first";
645 sFilterRow = "tr:first";
647 oHost = oTable.fnSettings().nTHead;
652 //$(sFilterRow + " th", oHost).each(function (index) {//bug with ColVis
653 $(aoFilterCells).each(function (index) {//fix for ColVis
655 var aoColumn = { type: "text",
661 if (properties.aoColumns != null) {
662 if (properties.aoColumns.length < i || properties.aoColumns[i] == null)
664 aoColumn = properties.aoColumns[i];
666 //label = $(this).text(); //Before fix for ColVis
667 label = $($(this)[0].cell).text(); //Fix for ColVis
668 if (aoColumn.sSelector == null) {
669 //th = $($(this)[0]);//Before fix for ColVis
670 th = $($(this)[0].cell); //Fix for ColVis
673 th = $(aoColumn.sSelector);
675 th = $($(this)[0].cell);
678 if (aoColumn != null) {
679 if (aoColumn.sRangeFormat != null)
680 sRangeFormat = aoColumn.sRangeFormat;
682 sRangeFormat = properties.sRangeFormat;
683 switch (aoColumn.type) {
687 fnCreateInput(oTable, true, false, true, aoColumn.iFilterLength, aoColumn.iMaxLenght);
690 if (aoColumn.bRegex != true)
691 aoColumn.bRegex = false;
692 fnCreateSelect(oTable, aoColumn.values, aoColumn.bRegex, aoColumn.selected);
695 fnCreateRangeInput(oTable);
698 fnCreateDateRangeInput(oTable);
701 fnCreateCheckbox(oTable, aoColumn.values);
705 bRegex = (aoColumn.bRegex == null ? false : aoColumn.bRegex);
706 bSmart = (aoColumn.bSmart == null ? false : aoColumn.bSmart);
707 fnCreateInput(oTable, bRegex, bSmart, false, aoColumn.iFilterLength, aoColumn.iMaxLenght);
714 for (j = 0; j < aiCustomSearch_Indexes.length; j++) {
715 //var index = aiCustomSearch_Indexes[j];
716 var fnSearch_ = function () {
717 var id = oTable.attr("id");
718 return $("#" + id + "_range_from_" + aiCustomSearch_Indexes[j]).val() + properties.sRangeSeparator + $("#" + id + "_range_to_" + aiCustomSearch_Indexes[j]).val()
720 afnSearch_.push(fnSearch_);
723 if (oTable.fnSettings().oFeatures.bServerSide) {
725 var fnServerDataOriginal = oTable.fnSettings().fnServerData;
727 oTable.fnSettings().fnServerData = function (sSource, aoData, fnCallback) {
729 for (j = 0; j < aiCustomSearch_Indexes.length; j++) {
730 var index = aiCustomSearch_Indexes[j];
732 for (k = 0; k < aoData.length; k++) {
733 if (aoData[k].name == "sSearch_" + index)
734 aoData[k].value = afnSearch_[j]();
737 aoData.push({ "name": "sRangeSeparator", "value": properties.sRangeSeparator });
739 if (fnServerDataOriginal != null) {
741 fnServerDataOriginal(sSource, aoData, fnCallback, oTable.fnSettings()); //TODO: See Issue 18
743 fnServerDataOriginal(sSource, aoData, fnCallback);
747 $.getJSON(sSource, aoData, function (json) {