Nie wszystko otrzymujemy w pakiecie, inaczej nie nazywalibyśmy się programistami. Nieprawdaż? Takie wyzwanie stanęło niedawno przede mną podczas „chwilowych” prac z biblioteką jQuery EasyUI. Zadanie było proste – grid. Grid z danymi, grid z filtrami. Wszystko pięknie. Sam grid jest wsparty natywnie biblioteką, filtry obsługiwane są przez rozszerzenie. Filtr daty również istnieje. Ale, no właśnie – filtrowanie daty po przedziale to już miejsce na popis programistów. Nawet nie w całości, jeśli dokładnie śledzimy sieć. Zachęcam to lektury.

O datagridzie przeczytacie tutaj. O rozszerzeniach – filtrowaniu grida – dowiecie się więcej z tej strony. Zmierzamy w dobrym kierunku. Przyglądając się domyślnym operatorom filtrowania, można wywnioskować, że działają one typowo wspierając treść (String) lub liczby (Number). Dociekliwych odsyłam do źródła otwartego kodu – tutaj.

Zatem możemy dodać filtr daty, ale wyłącznie porównując wartość komórki do jednej wskazanej wartości podanej w filtrze. To nas nie interesuje, więc musimy coś dodać. Najpierw chwila poszukiwań rozwiązań po sieci i … mamy jeden traf. Jest rozwiązanie, ale jednak niekompletne. To nie problem. W tym artykule sprawimy, by stało się kompletne.

Wspomniane rozwiązanie pochodzi z posta na stronie forum jeasyui. Nie czytając przedstawionego kodu, można się spodziewać iż zadziała on tak, jak chcieliśmy. Niemałe musi być zdziwienie osób, które tak podeszły do problemu 🙂

Brak reguły – operator

Brak reguły operatora to największy problem w tym momencie. Wyzwanie … czyli to co lubimy 🙂 Kod operatora jest niezbędny. Wnioskując z operatorów domyślnych, musi on posiadać właściwość text określającą nazwę operatora, oraz isMatch określającą funkcję dopasowania.

$.extend($.fn.datagrid.defaults.operators, {
    inrange: {
         text: 'In range',
         isMatch: function(source, value) {
             if(value == ':' ) {
                 return true;
             }
             if(value != ':' && (source == undefined || source == '')) {
                 return false;
             }
             var date = value.split(':' );
             var sourcedate = new Date(source);

             if(date[0] == '' && date[1] != '') {
                 var enddate = new Date(date[1]);
                 return sourcedate <= enddate;
             }

             if(date[1] == '' && date[0] != '') {
                 var startdate = new Date(date[0]);
                 return sourcedate >= startdate;
             }

             var startdate = new Date(date[0]);
             var enddate = new Date(date[1]);

             return (sourcedate >= startdate && sourcedate <= enddate);
         }
    }
});

Jeśli wartość filtra jest dwukropkiem (brak przedziału) uznajemy źródło jako dopasowane i wiersz przechodzi do wyświetlenia (jeśli pozostałe filtry przeszły pomyślnie). Gdy przedział jest podany (nawet jednostronnie), a źródło jest puste odrzucamy wiersz nie przechodząc do algorytmu (działamy tam na określonych wartościach źródła, które nie może być nieokreślone).

Następnie wykonujemy operacje na datach, po kolei: dla przedziału domkniętego z prawej strony, z lewej strony, obustronnego w zależności od tego, jaki przedział jest podany w filtrze.

Operator jest kompletny, ale to jeszcze nie rozwiązanie.

Inicjalizacja filtra

O ile w tym momencie mamy już działający filtr o tyle aktualizacja danych następuje wyłącznie po kliknięciu na typ filtra po każdorazowej zmianie wartości przedziału. To jest uciążliwe i niezgodne z zasadami użyteczności. Nie ma co do tego żadnych wątpliwości.

Zatem do dzieła – rozszerzamy, podane na stronie forum, rozwiązanie:

$.extend($.fn.datagrid.defaults.filters, {
    dateRange: {
         init: function(container, options){
             var c = $('<div style="display:inline-block"><input class="d1"><input class="d2"></div>').appendTo(container);
             c.find('.d1,.d2').datebox({
                 onSelect: function(date){
                     var name = $(this).closest('div.datagrid-filter').attr('name');
                     var value = c.find('.d1').datebox('getValue')+':'+c.find('.d2').datebox('getValue');
                     $('#dg').datagrid('removeFilterRule', name);
                     $('#dg').datagrid('addFilterRule', {
                         field: name,
                         op: 'inrange',
                         value: value
                     });
                     $('#dg').datagrid('doFilter');
                 },
                 onChange: function(dateN, dateO){
                     if(dateN == '') {
                         var name = $(this).closest('div.datagrid-filter').attr('name');
                         var value = c.find('.d1').datebox('getValue')+':'+c.find('.d2').datebox('getValue');
                         $('#dg').datagrid('removeFilterRule', name);
                         $('#dg').datagrid('addFilterRule', {
                             field: name,
                             op: 'inrange',
                             value: value
                         });
                         $('#dg').datagrid('doFilter');
                     }
                 }
             });
             $('.d1+span>input[type="text"], .d2+span>input[type="text"]').addClass('datagrid-filter-range').on('blur', function() {
                 var name = $(this).closest('div.datagrid-filter').attr('name');
                 var value = c.find('.d1').datebox('getValue')+':'+c.find('.d2').datebox('getValue');
                 var dv = ($('#dg').datagrid('getFilterRule', name) ? $('#dg').datagrid('getFilterRule', name).value : ':' );
                 if(dv != value) {
                     $(this).closest('span.datebox').prev().datebox('setValue', value);
                     $('#dg').datagrid('removeFilterRule', name);
                     $('#dg').datagrid('addFilterRule', {
                         field: name,
                         op: 'inrange',
                         value: value
                     });
                     $('#dg').datagrid('doFilter');
                 }
             });
             return c;
         },
         destroy: function(target){
             $(target).find('.d1,.d2').datebox('destroy');
         },
         getValue: function(target){
             var d1 = $(target).find('.d1');
             var d2 = $(target).find('.d2');
             return d1.datebox('getValue') + ':'+d2.datebox('getValue');
         },
         setValue: function(target, value){
             var d1 = $(target).find('.d1');
             var d2 = $(target).find('.d2');
             var vv = value.split(':' );
             d1.datebox('setValue', vv[0]);
             d2.datebox('setValue', vv[1]);
         },  
         resize: function(target, width){
             $(target)._outerWidth(width)._outerHeight(22);
             $(target).find('.d1,.d2').datebox('resize', width/2);
         }
    }
});

Inicjalizacja została rozszerzona o obsługę zdarzeń onSelect onChange. Nie ma tam nic innego jak aktywacja określonej reguły filtru opartej o wybraną wartość przedziału. Funkcja onChange jest konieczna, gdy zerujemy wartość przedziału (wtedy nie można wybrać wartości pustej, trzeba z klawiatury wyczyścić pole) lub zmianiamy wartość przy pomocy klawiatury – wtedy jednak kontrolka datebox waliduje wartość i gdy dopasuje wywołuje zdarzenie onSelect. Dlatego zdarzenie onChange wyklucza taki przypadek podczas wykonywania funkcji. Gdyby nie było tam bloku if, pod uwagę byłyby brane wszystkie zdarzenia – również te, które przyszły po programowym wykonaniu kodu zmiany wartości – taka pętelka 🙂

Pozostały kod onBlur działa na kontrolkach widocznych (te które wspierają przedział bezpośrednio są ukryte!). Otóż gdy zmianimy przedział za pomocą klawiatury, a nie kontrolki datebox występują problemy przy wywoływaniu zdarzenia odświeżania filtrowania – nie zawsze to działało. Stąd ten fix. Pobieramy wartość reguły filta – jeśli jest inna niż wartość po zdarzeniu onBlur to ustalana jest nowa reguła filtrowania. Krótko i na temat.

Działający kompletny przykład znajdziecie pod tym linkiem. Przykład jest rozszerzeniem demo DataGrid Filter Row ze strony demo.