Merge branch 'maint/7.0'
[ninja.git] / application / media / js / jquery.filterable.js
blobf1d6da330fd3b646ee49f0a89eeeb499655b3ee4
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 ) {
12                         var type = select.attr( 'data-type' ),
13                                 root = settings.host + _site_domain + _index_page;
15                         return root + '/listview/fetch_ajax?query=[' + type + 's] all&columns[]=key&limit=1000000';
17                 },
19                 "collector": function ( select, data ) {
21                         var names = [];
22                         for ( var i = 0; i < data.data.length; i++ ) {
23                                 names.push( data.data[ i ].key );
24                         }
25                         select.filterable( names );
27                 },
29                 "ajax": {
30                         dataType: 'json',
31                         error: function( xhr ) {
32                                 console.log( xhr.responseText );
33                         }
34                 }
36         };
38         var getBoxing = function ( filtered, multi ) {
40                 if ( multi ) {
41                         return $( '<div class="jq-filterable-box">' ).append(
42                                 $( '<div class="jq-filterable-left">' ).append(
43                                         $( '<input type="text" class="jq-filterable-filter jq-filterable-larger" placeholder="Search...">' ),
44                                         $( '<input type="button" value="➤" class="jq-filterable-move" title="Use objects matching search">' ),
45                                         "<br>",
46                                         filtered.clone()
47                                                 .addClass( "jq-filterable-list" ),
48                                         "<br>",
49                                         $( '<div class="jq-filterable-stats jq-filterable-larger">' )
50                                 ),$( '<div class="jq-filterable-right">' ).append(
51                                         $( '<select multiple class="jq-filterable-results">' ),
52                                         "<br>",
53                                         $( '<div class="jq-filterable-result-stats jq-filterable-larger">' ).append( "No items selected..." )
54                                 )
55                         );
56                 } else {
57                         return $( '<div class="jq-filterable-box">' ).append(
58                                 $( '<div class="jq-filterable-left">' ).append(
59                                         $( '<input type="text" class="jq-filterable-filter" placeholder="Search...">' ),
60                                         filtered.clone()
61                                                 .addClass( "jq-filterable-list" ),
62                                         "<br>",
63                                         $( '<div class="jq-filterable-stats jq-filterable-largest">' )
64                                 )
65                         );
66                 }
68         }
70         var Filterable = function Filterable ( filtered, data ) {
72                 var defaults = [];
74                 if ( filtered.find( 'option' ).length > 0 ) {
75                         filtered.find( 'option' ).each( function ( i, e ) {
76                                 defaults.push( e.value );
77                         } );
78                 }
80                 var self = this;
81                 this.box = null;
82                 this.matching = 0;
84                 if ( filtered.attr( "multiple" ) ) {
85                         this.box = getBoxing( filtered, true );
86                         this.multiple = true;
87                 } else {
88                         this.box = getBoxing( filtered, false );
89                         this.multiple = false;
90                 }
92                 if (!this.multiple && !filtered.attr('data-required')) {
93                         data.unshift('');
94                 }
96                 this.results =                  new Set();
97                 this.memory =                           new Set();
98                 this.data =                                     new Set( data );
100                 this.filter =                           this.box.find( ".jq-filterable-filter" );
101                 this.filtered =                 this.box.find( ".jq-filterable-list" );
103                 this.statusbar =                this.box.find( '.jq-filterable-stats' );
104                 this.selected =                 this.box.find( '.jq-filterable-results' );
105                 this.resultstats =      this.box.find( '.jq-filterable-result-stats' );
106                 this.mover =                            this.box.find( '.jq-filterable-move' );
108                 this.form = filtered.closest("form");
109                 this.form.on( "submit", function ( e ) {
110                         self.selected.find( "option" ).attr( "selected", true );
111                 } );
113                 if ( this.multiple ) {
115                         this.selected.attr( "id", this.filtered.attr( "id" ) );
116                         this.selected.attr( "name", this.filtered.attr( "name" ) );
118                         this.filtered.attr("id", this.selected.attr("id").replace('[', '_tmp['));
119                         this.filtered.removeAttr( "name" );
121                 }
123                 filtered.replaceWith( this.box );
125                 var _default = filtered.find( ':selected' );
127                 if ( _default.length > 0 ) {
128                         this.search( this.filter.val(), _default.val() );
129                         if ( this.multiple && defaults.length > 0 ) {
130                                 defaults = new Set( defaults );
131                                 this.add( defaults );
132                         }
133                 } else {
134                         this.search( this.filter.val() );
135                         if ( defaults.length > 0 ) {
136                                 defaults = new Set( defaults );
137                                 this.add( defaults );
138                         }
139                 }
141                 // Add relevant events
143                 var key_timeout = null;
144                 this.box.on( "keyup", ".jq-filterable-filter", function ( e ) {
146                         if ( $.inArray( e.which, [ 37, 38, 39, 40 ] ) >= 0 ) return;
147                         else if ( e.which == 13 ) {
149                                 clearTimeout( key_timeout );
150                                 self.search( self.filter.val() );
152                         } else {
154                                 clearTimeout( key_timeout );
155                                 key_timeout = setTimeout( function () {
156                                         self.search( self.filter.val() );
157                                 }, 250 );
159                         }
161                 } );
163                 this.box.on('click', '.deselect_all', function( e ) {
164                         e.preventDefault();
165                         self.reset();
166                 });
168                 if ( this.multiple ) {
170                         this.box.on( "change", ".jq-filterable-list, .jq-filterable-results", function ( e ) {
172                                 var parent = $( e.target ),
173                                         values = null;
175                                 if ( parent.is( "option" ) ) {
176                                         parent = parent.closest( 'select' );
177                                 }
179                                 values = parent.val();
180                                 values = new Set( values );
182                                 if ( parent[0] == self.selected[0] )
183                                         self.remove( values );
184                                 else self.add( values );
186                         } );
188                         this.mover.on( "click", function () {
190                                 var values = self.search( self.filter.val(), null, true );
191                                 self.add( values );
193                         } );
195                 }
197         };
199         Filterable.prototype.batcher = function batcher ( set ) {
201                 var iterator = new SetIterator( set ),
202                         self = this;
204                 this.selected.empty();
206                 return function () {
208                         var fragment = document.createDocumentFragment(),
209                                 counter = 0,
210                                 index = null,
211                                 opt = null;
213                         while ( index = iterator.next() ) {
215                                 opt = document.createElement( 'option' );
216                                 opt.innerHTML = index;
217                                 opt.value = index;
219                                 fragment.appendChild( opt );
221                                 counter++;
222                                 if ( counter > 1000 ) break;
224                         }
226                         self.selected.append( fragment );
227                         return ( counter < 1000 );
229                 }
231         };
233         Filterable.prototype.add = function add ( set ) {
235                 var self = this;
237                 this.memory.reset();
238                 this.memory = set.union( this.memory );
240                 this.filtered.attr( 'disabled', 'disabled' );
241                 this.box.addClass( 'jq-filterable-working' );
243                 this.form.find( 'input[type="submit"]' )
244                         .attr( 'disabled', 'disabled' );
246                 var batch = this.batcher( this.memory ),
247                         completed = batch(),
248                         interval = setInterval( function () {
250                                 completed = batch();
251                                 if ( completed ) {
253                                         clearInterval( interval );
255                                         self.filtered.attr( 'disabled', false );
256                                         self.form.find( 'input[type="submit"]' )
257                                                 .attr( 'disabled', false );
259                                         self.box.removeClass( 'jq-filterable-working' );
260                                         self.search( self.filter.val() );
262                                 }
263                         }, 10 );
265         };
267         Filterable.prototype.remove = function remove ( set ) {
269                 var iterator = new SetIterator( set ),
270                         index = null, i = null;
272                 while ( index = iterator.next() ) {
273                         i = this.memory.find( index );
274                         if ( i >= 0 ) this.memory.remove( i );
275                         this.selected.find( 'option[value="' + index + '"]' ).remove();
276                 }
278                 this.search( this.filter.val() );
280         };
282         Filterable.prototype.note = function note ( message, type ) {
284                 this.statusbar.html( message );
286                 if ( type && type == "error" )
287                         this.statusbar.attr( "data-state", "error" );
288                 else if ( type && type == "warning" )
289                         this.statusbar.attr( "data-state", "warning" );
290                 else this.statusbar.attr( "data-state", "info" );
292         };
294         Filterable.prototype.error = function error ( message ) {
296                 this.filter.css( { "border-color": "#f40" } );
297                 this.note( message, "error" );
299         };
301         Filterable.prototype.reset = function reset () {
303                 this.memory.empty();
304                 this.selected.empty();
305                 this.search( this.filter.val() );
307         };
309         Filterable.prototype.update_labels = function update_labels ( ) {
311                 if ( this.matching >= settings.limit ) {
312                         this.note( "Not all items shown; " + this.matching + "/" + this.data.size() );
313                 } else {
314                         this.note( this.matching + " Items" );
315                 }
317                 // Fixes IE 9 error with dynamic options
318                 this.selected.css( 'width', '0px' );
319                 this.selected.css( 'width', '' );
321                 if( this.memory.size() > 0 ) {
322                         this.resultstats.html( this.memory.size() + " items selected. <a href='#' class='deselect_all'>Deselect all</a>" );
323                 } else {
324                         this.resultstats.text( "No items selected..." );
325                 }
327         };
329         /** method search ( string term )
330           *
331           * Searches the data array for regexp matches
332           * against term, then runs method populate.
333           *
334           * @param string term
335           * @param boolean respond
336           * @return void
337           */
338         Filterable.prototype.search = function search ( term, source, respond ) {
340                 var memresult = [];
341                 this.results = new Set();
343                 try {
344                         term = new RegExp( term, "i" );
345                 } catch ( e ) {
346                         this.error( "Invalid search ( " + e.message + " ) " );
347                         return;
348                 }
350                 var iterator = new SetIterator( this.data ),
351                         index = null;
353                 while ( ( index = iterator.next() ) != null ) {
354                         if ( index.match( term ) )
355                                 this.results.push( index );
356                 }
358                 this.memory.reset();
359                 this.results.reset();
361                 this.results = this.results.diff( this.memory );
362                 this.matching = this.results.size();
364                 if ( respond ) {
365                         this.results.reset();
366                         return this.results;
367                 } else {
368                         this.results.shrink( 0, settings.limit );
369                         this.populate( source );
370                 }
372         };
374         /** method populate ( string array data )
375           *
376           * Searches the data array for regexp matches
377           * against term, then runs method populate.
378           *
379           * @param string term
380           * @return void
381           */
382         Filterable.prototype.populate = function populate ( source ) {
384                 var fragment = document.createDocumentFragment(),
385                         iterator = null,
386                         opt = null,
387                         index = 0;
389                 iterator = new SetIterator( this.results );
391                 while ( ( index = iterator.next() ) != null ) {
393                         opt = document.createElement( 'option' );
394                         opt.innerHTML = index;
395                         opt.value = index;
397                         fragment.appendChild( opt );
399                 }
401                 this.filtered.empty();
402                 this.filtered.append( fragment );
403                 this.update_labels();
405                 if ( source ) {
406                         this.filtered.val( source );
407                 }
409                 if ( this.multiple ) {
410                         this.filtered.val([]);
411                 }
413         };
415         var Filterables = [];
416         var FilterableFactory = function FilterableFactory ( data ) {
418                 var F = ( new Filterable( this, data ) );
419                 Filterables.push( F );
420                 return F;
422         };
424         jQuery.filterable_settings = function ( key, value ) {
425                 if ( settings[ key ] ) {
426                         settings[ key ] = value;
427                 }
428         }
430         jQuery.fn.filterable = FilterableFactory;
431         jQuery.fn.filterable.find = function ( element ) {
433                 for ( var i = 0; i < Filterables.length; i++ ) {
434                         if ( Filterables[ i ].selected[ 0 ] == element[ 0 ] ) {
435                                 return Filterables[ i ];
436                         }
437                 }
439                 return null;
441         };
443         function selectload ( index, element ) {
445                 var select = $( element );
447                 if ( select.attr( 'data-type' ) ) {
449                         settings.ajax.success = function ( data ) {
450                                 settings.collector( select, data );
451                         };
453                         settings.ajax.url = settings.datasource( select );
454                         $.ajax( settings.ajax );
456                 } else if (select.length) {
458                         var options = $.map( select.children(), function( option ) {
459                                         return option.text;
460                                 });
462                         select.children().each( function() {
463                                 if (!$(this).attr('selected')) {
464                                         select.removeOption(this.text);
465                                 }
466                         } );
468                         select.filterable( options );
470                 }
472         }
474         $( document ).ready( function () {
475                 var selects = $( settings.selector );
476                 selects.each( selectload );
477         } );
479 } ) ( jQuery, window );