Moved filterable changes from nacoma to reports
[ninja.git] / application / media / js / jquery.filterable.js
blobdeaf3474e5b736f5e2fe001563e8239cded86c5d
2 ( function ( jQuery, global ) {
4 var settings = {
6 "limit": 1000,
7 "selector": "select[data-filterable]",
8 "host": window.location.protocol + "//" + window.location.host,
10 "datasource": function ( select ) {
11 var type = select.attr( 'data-type' );
12 return settings.host + '/api/status/' + type
15 "collector": function ( select, data ) {
16 var names = [];
17 for ( var i = 0; i < data.length; i++ )
18 names.push( data[ i ].name );
19 select.filterable( names );
22 "ajax": {
23 data: { 'format': 'json', csrf_token: _csrf_token, 'auth_method': 'ninja' },
24 dataType: 'json',
25 error: function( xhr ) {
26 console.log( xhr.responseText );
32 var getBoxing = function ( filtered, multi ) {
34 if ( multi ) {
35 return $( '<div class="jq-filterable-box">' ).append(
36 $( '<div class="jq-filterable-left">' ).append(
37 $( '<input type="text" class="jq-filterable-filter jq-filterable-larger" placeholder="Search...">' ),
38 $( '<input type="button" value="➤" class="jq-filterable-move" title="Use objects matching search">' ),
39 "<br>",
40 filtered.clone()
41 .addClass( "jq-filterable-list" ),
42 "<br>",
43 $( '<div class="jq-filterable-stats jq-filterable-larger">' )
44 ),$( '<div class="jq-filterable-right">' ).append(
45 $( '<select multiple class="jq-filterable-results">' ),
46 "<br>",
47 $( '<div class="jq-filterable-result-stats jq-filterable-larger">' ).append( "No items selected..." )
50 } else {
51 return $( '<div class="jq-filterable-box">' ).append(
52 $( '<div class="jq-filterable-left">' ).append(
53 $( '<input type="text" class="jq-filterable-filter" placeholder="Search...">' ),
54 filtered.clone()
55 .addClass( "jq-filterable-list" ),
56 "<br>",
57 $( '<div class="jq-filterable-stats jq-filterable-largest">' )
64 var Filterable = function Filterable ( filtered, data ) {
66 var defaults = [];
68 if ( filtered.find( 'option' ).length > 0 ) {
69 filtered.find( 'option' ).each( function ( i, e ) {
70 defaults.push( e.value );
71 } );
74 var self = this;
75 this.box = null;
76 this.matching = 0;
78 if ( filtered.attr( "multiple" ) ) {
79 this.box = getBoxing( filtered, true );
80 this.multiple = true;
81 } else {
82 this.box = getBoxing( filtered, false );
83 this.multiple = false;
86 if (!this.multiple && !filtered.attr('data-required')) {
87 data.unshift('');
90 this.results = new Set();
91 this.memory = new Set();
92 this.data = new Set( data );
94 this.filter = this.box.find( ".jq-filterable-filter" );
95 this.filtered = this.box.find( ".jq-filterable-list" );
97 this.statusbar = this.box.find( '.jq-filterable-stats' );
98 this.selected = this.box.find( '.jq-filterable-results' );
99 this.resultstats = this.box.find( '.jq-filterable-result-stats' );
100 this.mover = this.box.find( '.jq-filterable-move' );
102 this.form = filtered.closest("form");
103 this.form.on( "submit", function ( e ) {
104 self.selected.find( "option" ).attr( "selected", true );
105 } );
107 if ( this.multiple ) {
109 this.selected.attr( "id", this.filtered.attr( "id" ) );
110 this.selected.attr( "name", this.filtered.attr( "name" ) );
112 this.filtered.attr("id", this.selected.attr("id").replace('[', '_tmp['));
113 this.filtered.removeAttr( "name" );
117 filtered.replaceWith( this.box );
119 var _default = filtered.find( ':selected' );
121 if ( _default.length > 0 ) {
122 this.search( this.filter.val(), _default.val() );
123 if ( this.multiple && defaults.length > 0 ) {
124 defaults = new Set( defaults );
125 this.add( defaults );
127 } else {
128 this.search( this.filter.val() );
129 if ( defaults.length > 0 ) {
130 defaults = new Set( defaults );
131 this.add( defaults );
135 // Add relevant events
137 var key_timeout = null;
138 this.box.on( "keyup", ".jq-filterable-filter", function ( e ) {
140 if ( $.inArray( e.which, [ 37, 38, 39, 40 ] ) >= 0 ) return;
141 else if ( e.which == 13 ) {
143 clearTimeout( key_timeout );
144 self.search( self.filter.val() );
146 } else {
148 clearTimeout( key_timeout );
149 key_timeout = setTimeout( function () {
150 self.search( self.filter.val() );
151 }, 250 );
155 } );
157 this.box.on('click', '.deselect_all', function( e ) {
158 e.preventDefault();
159 self.reset();
162 if ( this.multiple ) {
164 this.box.on( "change", ".jq-filterable-list, .jq-filterable-results", function ( e ) {
166 var parent = $( e.target ),
167 values = null;
169 if ( parent.is( "option" ) ) {
170 parent = parent.closest( 'select' );
173 values = parent.val();
174 values = new Set( values );
176 if ( parent[0] == self.selected[0] )
177 self.remove( values );
178 else self.add( values );
180 } );
182 this.mover.on( "click", function () {
184 var values = self.search( self.filter.val(), null, true );
185 self.add( values );
187 } );
193 Filterable.prototype.batcher = function batcher ( set ) {
195 var iterator = new SetIterator( set ),
196 self = this;
198 this.selected.empty();
200 return function () {
202 var fragment = document.createDocumentFragment(),
203 counter = 0,
204 index = null,
205 opt = null;
207 while ( index = iterator.next() ) {
209 opt = document.createElement( 'option' );
210 opt.innerHTML = index;
211 opt.value = index;
213 fragment.appendChild( opt );
215 counter++;
216 if ( counter > 1000 ) break;
220 self.selected.append( fragment );
221 return ( counter < 1000 );
227 Filterable.prototype.add = function add ( set ) {
229 var self = this;
231 this.memory.reset();
232 this.memory = set.union( this.memory );
234 this.filtered.attr( 'disabled', 'disabled' );
235 this.box.addClass( 'jq-filterable-working' );
237 this.form.find( 'input[type="submit"]' )
238 .attr( 'disabled', 'disabled' );
240 var batch = this.batcher( this.memory ),
241 completed = batch(),
242 interval = setInterval( function () {
244 completed = batch();
245 if ( completed ) {
247 clearInterval( interval );
249 self.filtered.attr( 'disabled', false );
250 self.form.find( 'input[type="submit"]' )
251 .attr( 'disabled', false );
253 self.box.removeClass( 'jq-filterable-working' );
254 self.search( self.filter.val() );
257 }, 10 );
261 Filterable.prototype.remove = function remove ( set ) {
263 var iterator = new SetIterator( set ),
264 index = null, i = null;
266 while ( index = iterator.next() ) {
267 i = this.memory.find( index );
268 if ( i >= 0 ) this.memory.remove( i );
269 this.selected.find( 'option[value="' + index + '"]' ).remove();
272 this.search( this.filter.val() );
276 Filterable.prototype.note = function note ( message, type ) {
278 this.statusbar.html( message );
280 if ( type && type == "error" )
281 this.statusbar.attr( "data-state", "error" );
282 else if ( type && type == "warning" )
283 this.statusbar.attr( "data-state", "warning" );
284 else this.statusbar.attr( "data-state", "info" );
288 Filterable.prototype.error = function error ( message ) {
290 this.filter.css( { "border-color": "#f40" } );
291 this.note( message, "error" );
295 Filterable.prototype.reset = function reset () {
297 this.memory.empty();
298 this.selected.empty();
299 this.search( this.filter.val() );
303 Filterable.prototype.update_labels = function update_labels ( ) {
305 if ( this.matching >= settings.limit ) {
306 this.note( "Not all items shown; " + this.matching + "/" + this.data.size() );
307 } else {
308 this.note( this.matching + " Items" );
311 // Fixes IE 9 error with dynamic options
312 this.selected.css( 'width', '0px' );
313 this.selected.css( 'width', '' );
315 if( this.memory.size() > 0 ) {
316 this.resultstats.html( this.memory.size() + " items selected. <a href='#' class='deselect_all'>Deselect all</a>" );
317 } else {
318 this.resultstats.text( "No items selected..." );
323 /** method search ( string term )
325 * Searches the data array for regexp matches
326 * against term, then runs method populate.
328 * @param string term
329 * @param boolean respond
330 * @return void
332 Filterable.prototype.search = function search ( term, source, respond ) {
334 var memresult = [];
335 this.results = new Set();
337 try {
338 term = new RegExp( term, "i" );
339 } catch ( e ) {
340 this.error( "Invalid search ( " + e.message + " ) " );
341 return;
344 var iterator = new SetIterator( this.data ),
345 index = null;
347 while ( ( index = iterator.next() ) != null ) {
348 if ( index.match( term ) )
349 this.results.push( index );
352 this.memory.reset();
353 this.results.reset();
355 this.results = this.results.diff( this.memory );
356 this.matching = this.results.size();
358 if ( respond ) {
359 this.results.reset();
360 return this.results;
361 } else {
362 this.results.shrink( 0, settings.limit );
363 this.populate( source );
368 /** method populate ( string array data )
370 * Searches the data array for regexp matches
371 * against term, then runs method populate.
373 * @param string term
374 * @return void
376 Filterable.prototype.populate = function populate ( source ) {
378 var fragment = document.createDocumentFragment(),
379 iterator = null,
380 opt = null,
381 index = 0;
383 iterator = new SetIterator( this.results );
385 while ( ( index = iterator.next() ) != null ) {
387 opt = document.createElement( 'option' );
388 opt.innerHTML = index;
389 opt.value = index;
391 fragment.appendChild( opt );
395 this.filtered.empty();
396 this.filtered.append( fragment );
397 this.update_labels();
399 if ( source ) {
400 this.filtered.val( source );
403 if ( this.multiple ) {
404 this.filtered.val([]);
409 var Filterables = [];
410 var FilterableFactory = function FilterableFactory ( data ) {
412 var F = ( new Filterable( this, data ) );
413 Filterables.push( F );
414 return F;
418 jQuery.fn.filterable = FilterableFactory;
419 jQuery.fn.filterable.find = function ( element ) {
421 for ( var i = 0; i < Filterables.length; i++ ) {
422 if ( Filterables[ i ].selected[ 0 ] == element[ 0 ] ) {
423 return Filterables[ i ];
427 return null;
431 function selectload ( index, element ) {
433 var select = $( element );
435 if ( select.attr( 'data-type' ) ) {
437 settings.ajax.success = function ( data ) {
438 settings.collector( select, data );
441 settings.ajax.url = settings.datasource( select );
442 $.ajax( settings.ajax );
444 } else if (select.length) {
446 var options = $.map( select.children(), function( option ) {
447 return option.text;
450 select.children().each( function() {
451 if (!$(this).attr('selected')) {
452 select.removeOption(this.text);
454 } );
456 select.filterable( options );
462 $( document ).ready( function () {
463 var selects = $( settings.selector );
464 selects.each( selectload );
465 } );
467 } ) ( jQuery, window );