PageSourceSearch

https://pao-pao.net/sites/pao-pao.net/files/js/js_QInbTDX7VxlfwDuvRkOrCPYZrtjnshqz-tBcybFYXmA.js

js pao-pao.net collected 2026-09-24 22:12:50 UTC 76,228 bytes, 2,202 lines download raw bytes

1/*
2 * jQuery Dynatable plugin 0.3.1
3 *
4 * Copyright (c) 2014 Steve Schwartz (JangoSteve)
5 *
6 * Dual licensed under the AGPL and Proprietary licenses:
7 *   http://www.dynatable.com/license/
8 *
9 * Date: Tue Jan 02 2014
10 */
11//
12
13(function($) {
14  var defaults,
15      mergeSettings,
16      dt,
17      Model,
18      modelPrototypes = {
19        dom: Dom,
20        domColumns: DomColumns,
21        records: Records,
22        recordsCount: RecordsCount,
23        processingIndicator: ProcessingIndicator,
24        state: State,
25        sorts: Sorts,
26        sortsHeaders: SortsHeaders,
27        queries: Queries,
28        inputsSearch: InputsSearch,
29        paginationPage: PaginationPage,
30        paginationPerPage: PaginationPerPage,
31        paginationLinks: PaginationLinks
32      },
33      utility,
34      build,
35      processAll,
36      initModel,
37      defaultRowWriter,
38      defaultCellWriter,
39      defaultAttributeWriter,
40      defaultAttributeReader;
41
42  //-----------------------------------------------------------------
43  // Cached plugin global defaults
44  //-----------------------------------------------------------------
45
46  defaults = {
47    features: {
48      paginate: true,
49      sort: true,
50      pushState: true,
51      search: true,
52      recordCount: true,
53      perPageSelect: true
54    },
55    table: {
56      defaultColumnIdStyle: 'camelCase',
57      columns: null,
58      headRowSelector: 'thead tr', // or e.g. tr:first-child
59      bodyRowSelector: 'tbody tr',
60      headRowClass: null
61    },
62    inputs: {
63      queries: null,
64      sorts: null,
65      multisort: ['ctrlKey', 'shiftKey', 'metaKey'],
66      page: null,
67      queryEvent: 'blur change',
68      recordCountTarget: null,
69      recordCountPlacement: 'after',
70      paginationLinkTarget: null,
71      paginationLinkPlacement: 'after',
72      paginationClass: 'dynatable-pagination-links',
73      paginationLinkClass: 'dynatable-page-link',
74      paginationPrevClass: 'dynatable-page-prev',
75      paginationNextClass: 'dynatable-page-next',
76      paginationActiveClass: 'dynatable-active-page',
77      paginationDisabledClass: 'dynatable-disabled-page',
78      paginationPrev: 'Previous',
79      paginationNext: 'Next',
80      paginationGap: [1,2,2,1],
81      searchTarget: null,
82      searchPlacement: 'before',
83      perPageTarget: null,
84      perPagePlacement: 'before',
85      perPageText: 'Show: ',
86      recordCountText: 'Showing ',
87      processingText: 'Processing...'
88    },
89    dataset: {
90      ajax: false,
91      ajaxUrl: null,
92      ajaxCache: null,
93      ajaxOnLoad: false,
94      ajaxMethod: 'GET',
95      ajaxDataType: 'json',
96      totalRecordCount: null,
97      queries: {},
98      queryRecordCount: null,
99      page: null,
100      perPageDefault: 10,
101      perPageOptions: [10,20,50,100],
102      sorts: {},
103      sortsKeys: null,
104      sortTypes: {},
105      records: null
106    },
107    writers: {
108      _rowWriter: defaultRowWriter,
109      _cellWriter: defaultCellWriter,
110      _attributeWriter: defaultAttributeWriter
111    },
112    readers: {
113      _rowReader: null,
114      _attributeReader: defaultAttributeReader
115    },
116    params: {
117      dynatable: 'dynatable',
118      queries: 'queries',
119      sorts: 'sorts',
120      page: 'page',
121      perPage: 'perPage',
122      offset: 'offset',
123      records: 'records',
124      record: null,
125      queryRecordCount: 'queryRecordCount',
126      totalRecordCount: 'totalRecordCount'
127    }
128  };
129
130  //-----------------------------------------------------------------
131  // Each dynatable instance inherits from this,
132  // set properties specific to instance
133  //-----------------------------------------------------------------
134
135  dt = {
136    init: function(element, options) {
137      this.settings = mergeSettings(options);
138      this.element = element;
139      this.$element = $(element);
140
141      // All the setup that doesn't require element or options
142      build.call(this);
143
144      return this;
145    },
146
147    process: function(skipPushState) {
148      processAll.call(this, skipPushState);
149    }
150  };
151
152  //-----------------------------------------------------------------
153  // Cached plugin global functions
154  //-----------------------------------------------------------------
155
156  mergeSettings = function(options) {
157    var newOptions = $.extend(true, {}, defaults, options);
158
159    // TODO: figure out a better way to do this.
160    // Doing `extend(true)` causes any elements that are arrays
161    // to merge the default and options arrays instead of overriding the defaults.
162    if (options) {
163      if (options.inputs) {
164        if (options.inputs.multisort) {
165          newOptions.inputs.multisort = options.inputs.multisort;
166        }
167        if (options.inputs.paginationGap) {
168          newOptions.inputs.paginationGap = options.inputs.paginationGap;
169        }
170      }
171      if (options.dataset && options.dataset.perPageOptions) {
172        newOptions.dataset.perPageOptions = options.dataset.perPageOptions;
173      }
174    }
175
176    return newOptions;
177  };
178
179  build = function() {
180    this.$element.trigger('dynatable:preinit', this);
181
182    for (model in modelPrototypes) {
183      if (modelPrototypes.hasOwnProperty(model)) {
184        var modelInstance = this[model] = new modelPrototypes[model](this, this.settings);
185        if (modelInstance.initOnLoad()) {
186          modelInstance.init();
187        }
188      }
189    }
190
191    this.$element.trigger('dynatable:init', this);
192
193    if (!this.settings.dataset.ajax || (this.settings.dataset.ajax && this.settings.dataset.ajaxOnLoad) || this.settings.features.paginate) {
194      this.process();
195    }
196  };
197
198  processAll = function(skipPushState) {
199    var data = {};
200
201    this.$element.trigger('dynatable:beforeProcess', data);
202
203    if (!$.isEmptyObject(this.settings.dataset.queries)) { data[this.settings.params.queries] = this.settings.dataset.queries; }
204    // TODO: Wrap this in a try/rescue block to hide the processing indicator and indicate something went wrong if error
205    this.processingIndicator.show();
206
207    if (this.settings.features.sort && !$.isEmptyObject(this.settings.dataset.sorts)) { data[this.settings.params.sorts] = this.settings.dataset.sorts; }
208    if (this.settings.features.paginate && this.settings.dataset.page) {
209      var page = this.settings.dataset.page,
210          perPage = this.settings.dataset.perPage;
211      data[this.settings.params.page] = page;
212      data[this.settings.params.perPage] = perPage;
213      data[this.settings.params.offset] = (page - 1) * perPage;
214    }
215    if (this.settings.dataset.ajaxData) { $.extend(data, this.settings.dataset.ajaxData); }
216
217    // If ajax, sends query to ajaxUrl with queries and sorts serialized and appended in ajax data
218    // otherwise, executes queries and sorts on in-page data
219    if (this.settings.dataset.ajax) {
220      var _this = this;
221      var options = {
222        type: _this.settings.dataset.ajaxMethod,
223        dataType: _this.settings.dataset.ajaxDataType,
224        data: data,
225        error: function(xhr, error) {
226        },
227        success: function(response) {
228          _this.$element.trigger('dynatable:ajax:success', response);
229          // Merge ajax results and meta-data into dynatables cached data
230          _this.records.updateFromJson(response);
231          // update table with new records
232          _this.dom.update();
233
234          if (!skipPushState && _this.state.initOnLoad()) {
235            _this.state.push(data);
236          }
237        },
238        complete: function() {
239          _this.processingIndicator.hide();
240        }
241      };
242      // Do not pass url to `ajax` options if blank
243      if (this.settings.dataset.ajaxUrl) {
244        options.url = this.settings.dataset.ajaxUrl;
245
246      // If ajaxUrl is blank, then we're using the current page URL,
247      // we need to strip out any query, sort, or page data controlled by dynatable
248      // that may have been in URL when page loaded, so that it doesn't conflict with
249      // what's passed in with the data ajax parameter
250      } else {
251        options.url = utility.refreshQueryString(window.location.href, {}, this.settings);
252      }
253      if (this.settings.dataset.ajaxCache !== null) { options.cache = this.settings.dataset.ajaxCache; }
254
255      $.ajax(options);
256    } else {
257      this.records.resetOriginal();
258      this.queries.run();
259      if (this.settings.features.sort) {
260        this.records.sort();
261      }
262      if (this.settings.features.paginate) {
263        this.records.paginate();
264      }
265      this.dom.update();
266      this.processingIndicator.hide();
267
268      if (!skipPushState && this.state.initOnLoad()) {
269        this.state.push(data);
270      }
271    }
272    this.$element.trigger('dynatable:afterProcess', data);
273  };
274
275  function defaultRowWriter(rowIndex, record, columns, cellWriter) {
276    var tr = '';
277
278    // grab the record's attribute for each column
279    for (var i = 0, len = columns.length; i < len; i++) {
280      tr += cellWriter(columns[i], record);
281    }
282
283    return '<tr>' + tr + '</tr>';
284  };
285
286  function defaultCellWriter(column, record) {
287    var html = column.attributeWriter(record),
288        td = '<td';
289
290    if (column.hidden || column.textAlign) {
291      td += ' style="';
292
293      // keep cells for hidden column headers hidden
294      if (column.hidden) {
295        td += 'display: none;';
296      }
297
298      // keep cells aligned as their column headers are aligned
299      if (column.textAlign) {
300        td += 'text-align: ' + column.textAlign + ';';
301      }
302
303      td += '"';
304    }
305
306    return td + '>' + html + '</td>';
307  };
308
309  function defaultAttributeWriter(record) {
310    // `this` is the column object in settings.columns
311    // TODO: automatically convert common types, such as arrays and objects, to string
312    return record[this.id];
313  };
314
315  function defaultAttributeReader(cell, record) {
316    return $(cell).html();
317  };
318
319  //-----------------------------------------------------------------
320  // Dynatable object model prototype
321  // (all object models get these default functions)
322  //-----------------------------------------------------------------
323
324  Model = {
325    initOnLoad: function() {
326      return true;
327    },
328
329    init: function() {}
330  };
331
332  for (model in modelPrototypes) {
333    if (modelPrototypes.hasOwnProperty(model)) {
334      var modelPrototype = modelPrototypes[model];
335      modelPrototype.prototype = Model;
336    }
337  }
338
339  //-----------------------------------------------------------------
340  // Dynatable object models
341  //-----------------------------------------------------------------
342
343  function Dom(obj, settings) {
344    var _this = this;
345
346    // update table contents with new records array
347    // from query (whether ajax or not)
348    this.update = function() {
349      var rows = '',
350          columns = settings.table.columns,
351          rowWriter = settings.writers._rowWriter,
352          cellWriter = settings.writers._cellWriter;
353
354      obj.$element.trigger('dynatable:beforeUpdate', rows);
355
356      // loop through records
357      for (var i = 0, len = settings.dataset.records.length; i < len; i++) {
358        var record = settings.dataset.records[i],
359            tr = rowWriter(i, record, columns, cellWriter);
360        rows += tr;
361      }
362
363      // Appended dynatable interactive elements
364      if (settings.features.recordCount) {
365        $('#dynatable-record-count-' + obj.element.id).replaceWith(obj.recordsCount.create());
366      }
367      if (settings.features.paginate) {
368        $('#dynatable-pagination-links-' + obj.element.id).replaceWith(obj.paginationLinks.create());
369        if (settings.features.perPageSelect) {
370          $('#dynatable-per-page-' + obj.element.id).val(parseInt(settings.dataset.perPage));
371        }
372      }
373
374      // Sort headers functionality
375      if (settings.features.sort && columns) {
376        obj.sortsHeaders.removeAllArrows();
377        for (var i = 0, len = columns.length; i < len; i++) {
378          var column = columns[i],
379              sortedByColumn = utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; }),
380              value = settings.dataset.sorts[column.sorts[0]];
381
382          if (sortedByColumn) {
383            obj.$element.find('[data-dynatable-column="' + column.id + '"]').find('.dynatable-sort-header').each(function(){
384              if (value == 1) {
385                obj.sortsHeaders.appendArrowUp($(this));
386              } else {
387                obj.sortsHeaders.appendArrowDown($(this));
388              }
389            });
390          }
391        }
392      }
393
394      // Query search functionality
395      if (settings.inputs.queries || settings.features.search) {
396        var allQueries = settings.inputs.queries || $();
397        if (settings.features.search) {
398          allQueries = allQueries.add('#dynatable-query-search-' 
398+ obj.element.id);
399        }
400
401        allQueries.each(function() {
402          var $this = $(this),
403              q = settings.dataset.queries[$this.data('dynatable-query')];
404          $this.val(q || '');
405        });
406      }
407
408      obj.$element.find(settings.table.bodyRowSelector).remove();
409      obj.$element.append(rows);
410
411      obj.$element.trigger('dynatable:afterUpdate', rows);
412    };
413  };
414
415  function DomColumns(obj, settings) {
416    var _this = this;
417
418    this.initOnLoad = function() {
419      return obj.$element.is('table');
420    };
421
422    this.init = function() {
423      settings.table.columns = [];
424      this.getFromTable();
425    };
426
427    // initialize table[columns] array
428    this.getFromTable = function() {
429      var $columns = obj.$element.find(settings.table.headRowSelector).children('th,td');
430      if ($columns.length) {
431        $columns.each(function(index){
432          _this.add($(this), index, true);
433        });
434      } else {
435        return $.error("Couldn't find any columns headers in '" + settings.table.headRowSelector + " th,td'. If your header row is different, specify the selector in the table: headRowSelector option.");
436      }
437    };
438
439    this.add = function($column, position, skipAppend, skipUpdate) {
440      var columns = settings.table.columns,
441          label = $column.text(),
442          id = $column.data('dynatable-column') || utility.normalizeText(label, settings.table.defaultColumnIdStyle),
443          dataSorts = $column.data('dynatable-sorts'),
444          sorts = dataSorts ? $.map(dataSorts.split(','), function(text) { return $.trim(text); }) : [id];
445
446      // If the column id is blank, generate an id for it
447      if ( !id ) {
448        this.generate($column);
449        id = $column.data('dynatable-column');
450      }
451      // Add column data to plugin instance
452      columns.splice(position, 0, {
453        index: position,
454        label: label,
455        id: id,
456        attributeWriter: settings.writers[id] || settings.writers._attributeWriter,
457        attributeReader: settings.readers[id] || settings.readers._attributeReader,
458        sorts: sorts,
459        hidden: $column.css('display') === 'none',
460        textAlign: $column.css('text-align')
461      });
462
463      // Modify header cell
464      $column
465        .attr('data-dynatable-column', id)
466        .addClass('dynatable-head');
467      if (settings.table.headRowClass) { $column.addClass(settings.table.headRowClass); }
468
469      // Append column header to table
470      if (!skipAppend) {
471        var domPosition = position + 1,
472            $sibling = obj.$element.find(settings.table.headRowSelector)
473              .children('th:nth-child(' + domPosition + '),td:nth-child(' + domPosition + ')').first(),
474            columnsAfter = columns.slice(position + 1, columns.length);
475
476        if ($sibling.length) {
477          $sibling.before($column);
478        // sibling column doesn't yet exist (maybe this is the last column in the header row)
479        } else {
480          obj.$element.find(settings.table.headRowSelector).append($column);
481        }
482
483        obj.sortsHeaders.attachOne($column.get());
484
485        // increment the index of all columns after this one that was just inserted
486        if (columnsAfter.length) {
487          for (var i = 0, len = columnsAfter.length; i < len; i++) {
488            columnsAfter[i].index += 1;
489          }
490        }
491
492        if (!skipUpdate) {
493          obj.dom.update();
494        }
495      }
496
497      return dt;
498    };
499
500    this.remove = function(columnIndexOrId) {
501      var columns = settings.table.columns,
502          length = columns.length;
503
504      if (typeof(columnIndexOrId) === "number") {
505        var column = columns[columnIndexOrId];
506        this.removeFromTable(column.id);
507        this.removeFromArray(columnIndexOrId);
508      } else {
509        // Traverse columns array in reverse order so that subsequent indices
510        // don't get messed up when we delete an item from the array in an iteration
511        for (var i = columns.length - 1; i >= 0; i--) {
512          var column = columns[i];
513
514          if (column.id === columnIndexOrId) {
515            this.removeFromTable(columnIndexOrId);
516            this.removeFromArray(i);
517          }
518        }
519      }
520
521      obj.dom.update();
522    };
523
524    this.removeFromTable = function(columnId) {
525      obj.$element.find(settings.table.headRowSelector).children('[data-dynatable-column="' + columnId + '"]').first()
526        .remove();
527    };
528
529    this.removeFromArray = function(index) {
530      var columns = settings.table.columns,
531          adjustColumns;
532      columns.splice(index, 1);
533      adjustColumns = columns.slice(index, columns.length);
534      for (var i = 0, len = adjustColumns.length; i < len; i++) {
535        adjustColumns[i].index -= 1;
536      }
537    };
538
539    this.generate = function($cell) {
540      var cell = $cell === undefined ? $('<th></th>') : $cell;
541      return this.attachGeneratedAttributes(cell);
542    };
543
544    this.attachGeneratedAttributes = function($cell) {
545      // Use increment to create unique column name that is the same each time the page is reloaded,
546      // in order to avoid errors with mismatched attribute names when loading cached `dataset.records` array
547      var increment = obj.$element.find(settings.table.headRowSelector).children('th[data-dynatable-generated]').length;
548      return $cell
549        .attr('data-dynatable-column', 'dynatable-generated-' + increment) //+ utility.randomHash(),
550        .attr('data-dynatable-no-sort', 'true')
551        .attr('data-dynatable-generated', increment);
552    };
553  };
554
555  function Records(obj, settings) {
556    var _this = this;
557
558    this.initOnLoad = function() {
559      return !settings.dataset.ajax;
560    };
561
562    this.init = function() {
563      if (settings.dataset.records === null) {
564        settings.dataset.records = this.getFromTable();
565
566        if (!settings.dataset.queryRecordCount) {
567          settings.dataset.queryRecordCount = this.count();
568        }
569
570        if (!settings.dataset.totalRecordCount){
571          settings.dataset.totalRecordCount = settings.dataset.queryRecordCount;
572        }
573      }
574
575      // Create cache of original full recordset (unpaginated and unqueried)
576      settings.dataset.originalRecords = $.extend(true, [], settings.dataset.records);
577    };
578
579    // merge ajax response json with cached data including
580    // meta-data and records
581    this.updateFromJson = function(data) {
582      var records;
583      if (settings.params.records === "_root") {
584        records = data;
585      } else if (settings.params.records in data) {
586        records = data[settings.params.records];
587      }
588      if (settings.params.record) {
589        var len = records.length - 1;
590        for (var i = 0; i < len; i++) {
591          records[i] = records[i][settings.params.record];
592        }
593      }
594      if (settings.params.queryRecordCount in data) {
595        settings.dataset.queryRecordCount = data[settings.params.queryRecordCount];
596      }
597      if (settings.params.totalRecordCount in data) {
598        settings.dataset.totalRecordCount = data[settings.params.totalRecordCount];
599      }
600      settings.dataset.records = records;
601    };
602
603    // For really advanced sorting,
604    // see http://james.padolsey.com/javascript/sorting-elements-with-jquery/
605    this.sort = function() {
606      var sort = [].sort,
607          sorts = settings.dataset.sorts,
608          sortsKeys = settings.dataset.sortsKeys,
609          sortTypes = settings.dataset.sortTypes;
610
611      var sortFunction = function(a, b) {
612        var comparison;
613        if ($.isEmptyObject(sorts)) {
614          comparison = obj.sorts.functions['originalPlacement'](a, b);
615        } else {
616          for (var i = 0, len = sortsKeys.length; i < len; i++) {
617            var attr = sortsKeys[i],
618                direction = sorts[attr],
619                sortType = sortTypes[attr] || obj.sorts.guessType(a, b, attr);
620            comparison = obj.sorts.functions[sortType](a, b, attr, direction);
621            // Don't need to sort any further unless this sort is a tie between a and b,
622            // so break the for loop unless tied
623            if (comparison !== 0) { break; }
624          }
625        }
626        return comparison;
627      }
628
629      return sort.call(settings.dataset.records, sortFunction);
630    };
631
632    this.paginate = function() {
633      var bounds = this.pageBounds(),
634          first = bounds[0], last = bounds[1];
635      settings.dataset.records = settings.dataset.records.slice(first, last);
636    };
637
638    this.resetOriginal = function() {
639      settings.dataset.records = settings.dataset.originalRecords || [];
640    };
641
642    this.pageBounds = function() {
643      var page = settings.dataset.page || 1,
644          first = (page - 1) * settings.dataset.perPage,
645          last = Math.min(first + settings.dataset.perPage, settings.dataset.queryRecordCount);
646      return [first,last];
647    };
648
649    // get initial recordset to populate table
650    // if ajax, call ajaxUrl
651    // otherwise, initialize from in-table records
652    this.getFromTable = function() {
653      var records = [],
654          columns = settings.table.columns,
655          tableRecords = obj.$element.find(settings.table.bodyRowSelector);
656
657      tableRecords.each(function(index){
658        var record = {};
659        record['dynatable-original-index'] = index;
660        $(this).find('th,td').each(function(index) {
661          if (columns[index] === undefined) {
662            // Header cell didn't exist for this column, so let's generate and append
663            // a new header cell with a randomly generated name (so we can store and
664            // retrieve the contents of this column for each record)
665            obj.domColumns.add(obj.domColumns.generate(), columns.length, false, true);
665 // don't skipAppend, do skipUpdate
666          }
667          var value = columns[index].attributeReader(this, record),
668              attr = columns[index].id;
669
670          // If value from table is HTML, let's get and cache the text equivalent for
671          // the default string sorting, since it rarely makes sense for sort headers
672          // to sort based on HTML tags.
673          if (typeof(value) === "string" && value.match(/\s*\<.+\>/)) {
674            if (! record['dynatable-sortable-text']) {
675              record['dynatable-sortable-text'] = {};
676            }
677            record['dynatable-sortable-text'][attr] = $.trim($('<div></div>').html(value).text());
678          }
679
680          record[attr] = value;
681        });
682        // Allow configuration function which alters record based on attributes of
683        // table row (e.g. from html5 data- attributes)
684        if (typeof(settings.readers._rowReader) === "function") {
685          settings.readers._rowReader(index, this, record);
686        }
687        records.push(record);
688      });
689      return records; // 1st row is header
690    };
691
692    // count records from table
693    this.count = function() {
694      return settings.dataset.records.length;
695    };
696  };
697
698  function RecordsCount(obj, settings) {
699    this.initOnLoad = function() {
700      return settings.features.recordCount;
701    };
702
703    this.init = function() {
704      this.attach();
705    };
706
707    this.create = function() {
708      var recordsShown = obj.records.count(),
709          recordsQueryCount = settings.dataset.queryRecordCount,
710          recordsTotal = settings.dataset.totalRecordCount,
711          text = settings.inputs.recordCountText,
712          collection_name = settings.params.records;
713
714      if (recordsShown < recordsQueryCount && settings.features.paginate) {
715        var bounds = obj.records.pageBounds();
716        text += "<span class='dynatable-record-bounds'>" + (bounds[0] + 1) + " to " + bounds[1] + "</span> of ";
717      } else if (recordsShown === recordsQueryCount && settings.features.paginate) {
718        text += recordsShown + " of ";
719      }
720      text += recordsQueryCount + " " + collection_name;
721      if (recordsQueryCount < recordsTotal) {
722        text += " (filtered from " + recordsTotal + " total records)";
723      }
724
725      return $('<span></span>', {
726                id: 'dynatable-record-count-' + obj.element.id,
727                'class': 'dynatable-record-count',
728                html: text
729              });
730    };
731
732    this.attach = function() {
733      var $target = settings.inputs.recordCountTarget ? $(settings.inputs.recordCountTarget) : obj.$element;
734      $target[settings.inputs.recordCountPlacement](this.create());
735    };
736  };
737
738  function ProcessingIndicator(obj, settings) {
739    this.init = function() {
740      this.attach();
741    };
742
743    this.create = function() {
744      var $processing = $('<div></div>', {
745            html: '<span>' + settings.inputs.processingText + '</span>',
746            id: 'dynatable-processing-' + obj.element.id,
747            'class': 'dynatable-processing',
748            style: 'position: absolute; display: none;'
749          });
750
751      return $processing;
752    };
753
754    this.position = function() {
755      var $processing = $('#dynatable-processing-' + obj.element.id),
756          $span = $processing.children('span'),
757          spanHeight = $span.outerHeight(),
758          spanWidth = $span.outerWidth(),
759          $covered = obj.$element,
760          offset = $covered.offset(),
761          height = $covered.outerHeight(), width = $covered.outerWidth();
762
763      $processing
764        .offset({left: offset.left, top: offset.top})
765        .width(width)
766        .height(height)
767      $span
768        .offset({left: offset.left + ( (width - spanWidth) / 2 ), top: offset.top + ( (height - spanHeight) / 2 )});
769
770      return $processing;
771    };
772
773    this.attach = function() {
774      obj.$element.before(this.create());
775    };
776
777    this.show = function() {
778      $('#dynatable-processing-' + obj.element.id).show();
779      this.position();
780    };
781
782    this.hide = function() {
783      $('#dynatable-processing-' + obj.element.id).hide();
784    };
785  };
786
787  function State(obj, settings) {
788    this.initOnLoad = function() {
789      // Check if pushState option is true, and if browser supports it
790      return settings.features.pushState && history.pushState;
791    };
792
793    this.init = function() {
794      window.onpopstate = function(event) {
795        if (event.state && event.state.dynatable) {
796          obj.state.pop(event);
797        }
798      }
799    };
800
801    this.push = function(data) {
802      var urlString = window.location.search,
803          urlOptions,
804          path,
805          params,
806          hash,
807          newParams,
808          cacheStr,
809          cache,
810          // replaceState on initial load, then pushState after that
811          firstPush = !(window.history.state && window.history.state.dynatable),
812          pushFunction = firstPush ? 'replaceState' : 'pushState';
813
814      if (urlString && /^\?/.test(urlString)) { urlString = urlString.substring(1); }
815      $.extend(urlOptions, data);
816
817      params = utility.refreshQueryString(urlString, data, settings);
818      if (params) { params = '?' + params; }
819      hash = window.location.hash;
820      path = window.location.pathname;
821
822      obj.$element.trigger('dynatable:push', data);
823
824      cache = { dynatable: { dataset: settings.dataset } };
825      if (!firstPush) { cache.dynatable.scrollTop = $(window).scrollTop(); }
826      cacheStr = JSON.stringify(cache);
827
828      // Mozilla has a 640k char limit on what can be stored in pushState.
829      // See "limit" in https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#The_pushState().C2.A0method
830      // and "dataStr.length" in http://wine.git.sourceforge.net/git/gitweb.cgi?p=wine/wine-gecko;a=patch;h=43a11bdddc5fc1ff102278a120be66a7b90afe28
831      //
832      // Likewise, other browsers may have varying (undocumented) limits.
833      // Also, Firefox's limit can be changed in about:config as browser.history.maxStateObjectSize
834      // Since we don't know what the actual limit will be in any given situation, we'll just try caching and rescue
835      // any exceptions by retrying pushState without caching the records.
836      //
837      // I have absolutely no idea why perPageOptions suddenly becomes an array-like object instead of an array,
838      // but just recently, this started throwing an error if I don't convert it:
839      // 'Uncaught Error: DATA_CLONE_ERR: DOM Exception 25'
840      cache.dynatable.dataset.perPageOptions = $.makeArray(cache.dynatable.dataset.perPageOptions);
841
842      try {
843        window.history[pushFunction](cache, "Dynatable state", path + params + hash);
844      } catch(error) {
845        // Make cached records = null, so that `pop` will rerun process to retrieve records
846        cache.dynatable.dataset.records = null;
847        window.history[pushFunction](cache, "Dynatable state", path + params + hash);
848      }
849    };
850
851    this.pop = function(event) {
852      var data = event.state.dynatable;
853      settings.dataset = data.dataset;
854
855      if (data.scrollTop) { $(window).scrollTop(data.scrollTop); }
856
857      // If dataset.records is cached from pushState
858      if ( data.dataset.records ) {
859        obj.dom.update();
860      } else {
861        obj.process(true);
862      }
863    };
864  };
865
866  function Sorts(obj, settings) {
867    this.initOnLoad = function() {
868      return settings.features.sort;
869    };
870
871    this.init = function() {
872      var sortsUrl = window.location.search.match(new RegExp(settings.params.sorts + '[^&=]*=[^&]*', 'g'));
873      settings.dataset.sorts = sortsUrl ? utility.deserialize(sortsUrl)[settings.params.sorts] : {};
874      settings.dataset.sortsKeys = sortsUrl ? utility.keysFromObject(settings.dataset.sorts) : [];
875    };
876
877    this.add = function(attr, direction) {
878      var sortsKeys = settings.dataset.sortsKeys,
879          index = $.inArray(attr, sortsKeys);
880      settings.dataset.sorts[attr] = direction;
881      if (index === -1) { sortsKeys.push(attr); }
882      return dt;
883    };
884
885    this.remove = function(attr) {
886      var sortsKeys = settings.dataset.sortsKeys,
887          index = $.inArray(attr, sortsKeys);
888      delete settings.dataset.sorts[attr];
889      if (index !== -1) { sortsKeys.splice(index, 1); }
890      return dt;
891    };
892
893    this.clear = function() {
894      settings.dataset.sorts = {};
895      settings.dataset.sortsKeys.length = 0;
896    };
897
898    // Try to intelligently guess which sort function to use
899    // based on the type of attribute values.
900    // Consider using something more robust than `typeof` (http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/)
901    this.guessType = function(a, b, attr) {
902      var types = {
903            string: 'string',
904            number: 'number',
905            'boolean': 'number',
906            object: 'number' // dates and null values are also objects, this works...
907          },
908          attrType = a[attr] ? typeof(a[attr]) : typeof(b[attr]),
909          type = types[attrType] || 'number';
910      return type;
911    };
912
913    // Built-in sort functions
914    // (the most common use-cases I could think of)
915    this.functions = {
916      number: function(a, b, attr, direction) {
917        return a[attr] === b[attr] ? 0 : (direction > 0 ? a[attr] - b[attr] : b[attr] - a[attr]);
918      },
919      string: function(a, b, attr, direction) {
920        var aAttr = (a['dynatable-sortable-text'] && a['dynatable-sortable-text'][attr]) ? a['dynatable-sortable-text'][attr] : a[attr],
921            bAttr = (b['dynatable-sortable-text'] && b['dynatable-sortable-text'][attr]) ? b['dynatable-sortable-text'][attr] : b[attr],
922            comparison;
923        aAttr = aAttr.toLowerCase();
924        bAttr = bAttr.toLowerCase();
925        comparison = aAttr === bAttr ? 0 : (direction > 0 ? aAttr > bAttr : bAttr > aAttr);
926        // force false boolean value to -1, true to 1, and tie to 0
927        return comparison === false ? -1 : (comparison - 0);
928      },
929      originalPlacement: function(a, b) {
930        return a['dynatable-original-index'] - b['dynatable-original-index'];
931      }
932    };
933  };
934
935  // turn table headers into links which add sort to sorts array
936  function SortsHeaders(obj, settings) {
937    var _this = this;
938
939    this.initOnLoad = function() {
940      return settings.features.sort;
941    };
942
943    this.init = function() {
944      this.attach();
945    };
946
947    this.create = function(cell) {
948      var $cell = $(cell),
949          $link = $('<a></a>', {
950            'class': 'dynatable-sort-header',
951            href: '#',
952            html: $cell.html()
953          }),
954          id = $cell.data('dynatable-column'),
955          column = utility.findObjectInArray(settings.table.columns, {id: id});
956
957      $link.bind('click', function(e) {
958        _this.toggleSort(e, $link, column);
959        obj.process();
960
961        e.preventDefault();
962      });
963
964      if (this.sortedByColumn($link, column)) {
965        if (this.sortedByColumnValue(column) == 1) {
966          this.appendArrowUp($link);
967        } else {
968          this.appendArrowDown($link);
969        }
970      }
971
972      return $link;
973    };
974
975    this.removeAll = function() {
976      obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
977        _this.removeAllArrows();
978        _this.removeOne(this);
979      });
980    };
981
982    this.removeOne = function(cell) {
983      var $cell = $(cell),
984          $link = $cell.find('.dynatable-sort-header');
985      if ($link.length) {
986        var html = $link.html();
987        $link.remove();
988        $cell.html($cell.html() + html);
989      }
990    };
991
992    this.attach = function() {
993      obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
994        _this.attachOne(this);
995      });
996    };
997
998    this.attachOne = function(cell) {
999      var $cell = $(cell);
1000      if (!$cell.data('dynatable-no-sort')) {
1001        $cell.html(this.create(cell));
1002      }
1003    };
1004
1005    this.appendArrowUp = function($link) {
1006      this.removeArrow($link);
1007      $link.append("<span class='dynatable-arrow'> &#9650;</span>");
1008    };
1009
1010    this.appendArrowDown = function($link) {
1011      this.removeArrow($link);
1012      $link.append("<span class='dynatable-arrow'> &#9660;</span>");
1013    };
1014
1015    this.removeArrow = function($link) {
1016      // Not sure why `parent()` is needed, the arrow should be inside the link from `append()` above
1017      $link.find('.dynatable-arrow').remove();
1018    };
1019
1020    this.removeAllArrows = function() {
1021      obj.$element.find('.dynatable-arrow').remove();
1022    };
1023
1024    this.toggleSort = function(e, $link, column) {
1025      var sortedByColumn = this.sortedByColumn($link, column),
1026          value = this.sortedByColumnValue(column);
1027      // Clear existing sorts unless this is a multisort event
1028      if (!settings.inputs.multisort || !utility.anyMatch(e, settings.inputs.multisort, function(evt, key) { return e[key]; })) {
1029        this.removeAllArrows();
1030        obj.sorts.clear();
1031      }
1032
1033      // If sorts for this column are already set
1034      if (sortedByColumn) {
1035        // If ascending, then make descending
1036        if (value == 1) {
1037          for (var i = 0, len = column.sorts.length; i < len; i++) {
1038            obj.sorts.add(column.sorts[i], -1);
1039          }
1040          this.appendArrowDown($link);
1041        // If descending, remove sort
1042        } else {
1043          for (var i = 0, len = column.sorts.length; i < len; i++) {
1044            obj.sorts.remove(column.sorts[i]);
1045          }
1046          this.removeArrow($link);
1047        }
1048      // Otherwise, if not already set, set to ascending
1049      } else {
1050        for (var i = 0, len = column.sorts.length; i < len; i++) {
1051          obj.sorts.add(column.sorts[i], 1);
1052        }
1053        this.appendArrowUp($link);
1054      }
1055    };
1056
1057    this.sortedByColumn = function($link, column) {
1058      return utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; });
1059    };
1060
1061    this.sortedByColumnValue = function(column) {
1062      return settings.dataset.sorts[column.sorts[0]];
1063    };
1064  };
1065
1066  function Queries(obj, settings) {
1067    var _this = this;
1068
1069    this.initOnLoad = function() {
1070      return settings.inputs.queries || settings.features.search;
1071    };
1072
1073    this.init = function() {
1074      var queriesUrl = window.location.search.match(new RegExp(settings.params.queries + '[^&=]*=[^&]*', 'g'));
1075
1076      settings.dataset.queries = queriesUrl ? utility.deserialize(queriesUrl)[settings.params.queries] : {};
1077      if (settings.dataset.queries === "") { settings.dataset.queries = {}; }
1078
1079      if (settings.inputs.queries) {
1080        this.setupInputs();
1081      }
1082    };
1083
1084    this.add = function(name, value) {
1085      // reset to first page since query will change records
1086      if (settings.features.paginate) {
1087        settings.dataset.page = 1;
1088      }
1089      settings.dataset.queries[name] = value;
1090      return dt;
1091    };
1092
1093    this.remove = function(name) {
1094      delete settings.dataset.queries[name];
1095      return dt;
1096    };
1097
1098    this.run = function() {
1099      for (query in settings.dataset.queries) {
1100        if (settings.dataset.queries.hasOwnProperty(query)) {
1101          var value = settings.dataset.queries[query];
1102          if (_this.functions[query] === undefined) {
1103            // Try to lazily evaluate query from column names if not explicitly defined
1104            var queryColumn = utility.findObjectInArray(settings.table.columns, {id: query});
1105            if (queryColumn) {
1106              _this.functions[query] = function(record, queryValue) {
1107                return record[query] == queryValue;
1108              };
1109            } else {
1110              $.error("Query named '" + query + "' called, but not defined in queries.functions");
1111              continue; // to skip to next query
1112            }
1113          }
1114          // collect all records that return true for query
1115          settings.dataset.records = $.map(settings.dataset.records, function(record) {
1116            return _this.functions[query](record, value) ? record : null;
1117          });
1118        }
1119      }
1120      settings.dataset.queryRecordCount = obj.records.count();
1121    };
1122
1123    // Shortcut for performing simple query from built-in search
1124    this.runSearch = function(q) {
1125      var origQueries = $.extend({}, settings.dataset.queries);
1126      if (q) {
1127        this.add('search', q);
1128      } else {
1129        this.remove('search');
1130      }
1131      if (!utility.objectsEqual(settings.dataset.queries, origQueries)) {
1132        obj.process();
1133      }
1134    };
1135
1136    this.setupInputs = function() {
1137      settings.inputs.queries.each(function() {
1138        var $this = $(this),
1139            event = $this.data('dynatable-query-event') || settings.inputs.queryEvent,
1140            query = $this.data('dynatable-query') || $this.attr('name') || this.id,
1141            queryFunction = function(e) {
1142              var q = $(this).val();
1143              if (q === "") { q = undefined; }
1144              if (q === settings.dataset.queries[query]) { return false; }
1145              if (q) {
1146                _this.add(query, q);
1147              } else {
1148                _this.remove(query);
1149              }
1150              obj.process();
1151              e.preventDefault();
1152            };
1153
1154        $this
1155          .attr('data-dynatable-query', query)
1156          .bind(event, queryFunction)
1157          .bind('keypress', function(e) {
1158            if (e.which == 13) {
1159              queryFunction.call(this, e);
1160            }
1161          });
1162
1163        if (settings.dataset.queries[query]) { $this.val(decodeURIComponent(settings.dataset.queries[query])); }
1164      });
1165    };
1166
1167    // Query functions for in-page querying
1168    // each function should take a record and a value as input
1169    // and output true of false as to whether the record is a match or not
1170    this.functions = {
1171      search: function(record, queryValue) {
1172        var contains = false;
1173        // Loop through each attribute of record
1174        for (attr in record) {
1175          if (record.hasOwnProperty(attr)) {
1176            var attrValue = record[attr];
1177            if (typeof(attrValue) === "string" && attrValue.toLowerCase().indexOf(queryValue.toLowerCase()) !== -1) {
1178              contains = true;
1179              // Don't need to keep searching attributes once found
1180              break;
1181            } else {
1182              continue;
1183            }
1184          }
1185        }
1186        return contains;
1187      }
1188    };
1189  };
1190
1191  function InputsSearch(obj, settings) {
1192    var _this = this;
1193
1194    this.initOnLoad = function() {
1195      return settings.features.search;
1196    };
1197
1198    this.init = function() {
1199      this.attach();
1200    };
1201
1202    this.create = function() {
1203      var $search = $('<input />', {
1204            type: 'search',
1205            id: 'dynatable-query-search-' + obj.element.id,
1206            'data-dynatable-query': 'search',
1207            value: settings.dataset.queries.search
1208          }),
1209          $searchSpan = $('<span></span>', {
1210            id: 'dynatable-search-' + obj.element.id,
1211            'class': 'dynatable-search',
1212            text: 'Search: '
1213          }).append($search);
1214
1215      $search
1216        .bind(settings.inputs.queryEvent, function() {
1217          obj.queries.runSearch($(this).val());
1218        })
1219        .bind('keypress', function(e) {
1220          if (e.which == 13) {
1221            obj.queries.runSearch($(this).val());
1222            e.preventDefault();
1223          }
1224        });
1225      return $searchSpan;
1226    };
1227
1228    this.attach = function() {
1229      var $target = settings.inputs.searchTarget ? $(settings.inputs.searchTarget) : obj.$element;
1230      $target[settings.inputs.searchPlacement](this.create());
1231    };
1232  };
1233
1234  // provide a public function for selecting page
1235  function PaginationPage(obj, settings) {
1236    this.initOnLoad = function() {
1237      return settings.features.paginate;
1238    };
1239
1240    this.init = function() {
1241      var pageUrl = window.location.search.match(new RegExp(settings.params.page + '=([^&]*)'));
1242      // If page is present in URL parameters and pushState is enabled
1243      // (meaning that it'd be possible for dynatable to have put the
1244      // page parameter in the URL)
1245      if (pageUrl && settings.features.pushState) {
1246        this.set(pageUrl[1]);
1247      } else {
1248        this.set(1);
1249      }
1250    };
1251
1252    this.set = function(page) {
1253      settings.dataset.page = parseInt(page, 10);
1254    }
1255  };
1256
1257  function PaginationPerPage(obj, settings) {
1258    var _this = this;
1259
1260    this.initOnLoad = function() {
1261      return settings.features.paginate;
1262    };
1263
1264    this.init = function() {
1265      var perPageUrl = window.location.search.match(new RegExp(settings.params.perPage + '=([^&]*)'));
1266
1267      // If perPage is present in URL parameters and pushState is enabled
1268      // (meaning that it'd be possible for dynatable to have put the
1269      // perPage parameter in the URL)
1270      if (perPageUrl && settings.features.pushState) {
1271        // Don't reset page to 1 on init, since it might override page
1272        // set on init from URL
1273        this.set(perPageUrl[1], true);
1274      } else {
1275        this.set(settings.dataset.perPageDefault, true);
1276      }
1277
1278      if (settings.features.perPageSelect) {
1279        this.attach();
1280      }
1281    };
1282
1283    this.create = function() {
1284      var $select = $('<select>', {
1285            id: 'dynatable-per-page-' + obj.element.id,
1286            'class': 'dynatable-per-page-select'
1287          });
1288
1289      for (var i = 0, len = settings.dataset.perPageOptions.length; i < len; i++) {
1290        var number = settings.dataset.perPageOptions[i],
1291            selected = settings.dataset.perPage == number ? 'selected="selected"' : '';
1292        $select.append('<option value="' + number + '" ' + selected + '>' + number + '</option>');
1293      }
1294
1295      $select.bind('change', function(e) {
1296        _this.set($(this).val());
1297        obj.process();
1298      });
1299
1300      return $('<span />', {
1301        'class': 'dynatable-per-page'
1302      }).append("<span class='dynatable-per-page-label'>" + settings.inputs.perPageText + "</span>").append($select);
1303    };
1304
1305    this.attach = function() {
1306      var $target = settings.inputs.perPageTarget ? $(settings.inputs.perPageTarget) : obj.$element;
1307      $target[settings.inputs.perPagePlacement](this.create());
1308    };
1309
1310    this.set = function(number, skipResetPage) {
1311      if (!skipResetPage) { obj.paginationPage.set(1); }
1312      settings.dataset.perPage = parseInt(number);
1313    };
1314  };
1315
1316  // pagination links which update dataset.page attribute
1317  function PaginationLinks(obj, settings) {
1318    var _this = this;
1319
1320    this.initOnLoad = function() {
1321      return settings.features.paginate;
1322    };
1323
1324    this.init = function() {
1325      this.attach();
1326    };
1327
1328    this.create = function() {
1329      var pageLinks = '<ul id="' + 'dynatable-pagination-links-' + obj.element.i
1329d + '" class="' + settings.inputs.paginationClass + '">',
1330          pageLinkClass = settings.inputs.paginationLinkClass,
1331          activePageClass = settings.inputs.paginationActiveClass,
1332          disabledPageClass = settings.inputs.paginationDisabledClass,
1333          pages = Math.ceil(settings.dataset.queryRecordCount / settings.dataset.perPage),
1334          page = settings.dataset.page,
1335          breaks = [
1336            settings.inputs.paginationGap[0],
1337            settings.dataset.page - settings.inputs.paginationGap[1],
1338            settings.dataset.page + settings.inputs.paginationGap[2],
1339            (pages + 1) - settings.inputs.paginationGap[3]
1340          ];
1341
1342      pageLinks += '<li><span>Pages: </span></li>';
1343
1344      for (var i = 1; i <= pages; i++) {
1345        if ( (i > breaks[0] && i < breaks[1]) || (i > breaks[2] && i < breaks[3])) {
1346          // skip to next iteration in loop
1347          continue;
1348        } else {
1349          var li = obj.paginationLinks.buildLink(i, i, pageLinkClass, page == i, activePageClass),
1350              breakIndex,
1351              nextBreak;
1352
1353          // If i is not between one of the following
1354          // (1 + (settings.paginationGap[0]))
1355          // (page - settings.paginationGap[1])
1356          // (page + settings.paginationGap[2])
1357          // (pages - settings.paginationGap[3])
1358          breakIndex = $.inArray(i, breaks);
1359          nextBreak = breaks[breakIndex + 1];
1360          if (breakIndex > 0 && i !== 1 && nextBreak && nextBreak > (i + 1)) {
1361            var ellip = '<li><span class="dynatable-page-break">&hellip;</span></li>';
1362            li = breakIndex < 2 ? ellip + li : li + ellip;
1363          }
1364
1365          if (settings.inputs.paginationPrev && i === 1) {
1366            var prevLi = obj.paginationLinks.buildLink(page - 1, settings.inputs.paginationPrev, pageLinkClass + ' ' + settings.inputs.paginationPrevClass, page === 1, disabledPageClass);
1367            li = prevLi + li;
1368          }
1369          if (settings.inputs.paginationNext && i === pages) {
1370            var nextLi = obj.paginationLinks.buildLink(page + 1, settings.inputs.paginationNext, pageLinkClass + ' ' + settings.inputs.paginationNextClass, page === pages, disabledPageClass);
1371            li += nextLi;
1372          }
1373
1374          pageLinks += li;
1375        }
1376      }
1377
1378      pageLinks += '</ul>';
1379
1380      // only bind page handler to non-active and non-disabled page links
1381      var selector = '#dynatable-pagination-links-' + obj.element.id + ' a.' + pageLinkClass + ':not(.' + activePageClass + ',.' + disabledPageClass + ')';
1382      // kill any existing delegated-bindings so they don't stack up
1383      $(document).undelegate(selector, 'click.dynatable');
1384      $(document).delegate(selector, 'click.dynatable', function(e) {
1385        $this = $(this);
1386        $this.closest(settings.inputs.paginationClass).find('.' + activePageClass).removeClass(activePageClass);
1387        $this.addClass(activePageClass);
1388
1389        obj.paginationPage.set($this.data('dynatable-page'));
1390        obj.process();
1391        e.preventDefault();
1392      });
1393
1394      return pageLinks;
1395    };
1396
1397    this.buildLink = function(page, label, linkClass, conditional, conditionalClass) {
1398      var link = '<a data-dynatable-page=' + page + ' class="' + linkClass,
1399          li = '<li';
1400
1401      if (conditional) {
1402        link += ' ' + conditionalClass;
1403        li += ' class="' + conditionalClass + '"';
1404      }
1405
1406      link += '">' + label + '</a>';
1407      li += '>' + link + '</li>';
1408
1409      return li;
1410    };
1411
1412    this.attach = function() {
1413      // append page links *after* delegate-event-binding so it doesn't need to
1414      // find and select all page links to bind event
1415      var $target = settings.inputs.paginationLinkTarget ? $(settings.inputs.paginationLinkTarget) : obj.$element;
1416      $target[settings.inputs.paginationLinkPlacement](obj.paginationLinks.create());
1417    };
1418  };
1419
1420  utility = dt.utility = {
1421    normalizeText: function(text, style) {
1422      text = this.textTransform[style](text);
1423      return text;
1424    },
1425    textTransform: {
1426      trimDash: function(text) {
1427        return text.replace(/^\s+|\s+$/g, "").replace(/\s+/g, "-");
1428      },
1429      camelCase: function(text) {
1430        text = this.trimDash(text);
1431        return text
1432          .replace(/(\-[a-zA-Z])/g, function($1){return $1.toUpperCase().replace('-','');})
1433          .replace(/([A-Z])([A-Z]+)/g, function($1,$2,$3){return $2 + $3.toLowerCase();})
1434          .replace(/^[A-Z]/, function($1){return $1.toLowerCase();});
1435      },
1436      dashed: function(text) {
1437        text = this.trimDash(text);
1438        return this.lowercase(text);
1439      },
1440      underscore: function(text) {
1441        text = this.trimDash(text);
1442        return this.lowercase(text.replace(/(-)/g, '_'));
1443      },
1444      lowercase: function(text) {
1445        return text.replace(/([A-Z])/g, function($1){return $1.toLowerCase();});
1446      }
1447    },
1448    // Deserialize params in URL to object
1449    // see http://stackoverflow.com/questions/1131630/javascript-jquery-param-inverse-function/3401265#3401265
1450    deserialize: function(query) {
1451      if (!query) return {};
1452      // modified to accept an array of partial URL strings
1453      if (typeof(query) === "object") { query = query.join('&'); }
1454
1455      var hash = {},
1456          vars = query.split("&");
1457
1458      for (var i = 0; i < vars.length; i++) {
1459        var pair = vars[i].split("="),
1460            k = decodeURIComponent(pair[0]),
1461            v, m;
1462
1463        if (!pair[1]) { continue };
1464        v = decodeURIComponent(pair[1].replace(/\+/g, ' '));
1465
1466        // modified to parse multi-level parameters (e.g. "hi[there][dude]=whatsup" => hi: {there: {dude: "whatsup"}})
1467        while (m = k.match(/([^&=]+)\[([^&=]+)\]$/)) {
1468          var origV = v;
1469          k = m[1];
1470          v = {};
1471
1472          // If nested param ends in '][', then the regex above erroneously included half of a trailing '[]',
1473          // which indicates the end-value is part of an array
1474          if (m[2].substr(m[2].length-2) == '][') { // must use substr for IE to understand it
1475            v[m[2].substr(0,m[2].length-2)] = [origV];
1476          } else {
1477            v[m[2]] = origV;
1478          }
1479        }
1480
1481        // If it is the first entry with this name
1482        if (typeof hash[k] === "undefined") {
1483          if (k.substr(k.length-2) != '[]') { // not end with []. cannot use negative index as IE doesn't understand it
1484            hash[k] = v;
1485          } else {
1486            hash[k] = [v];
1487          }
1488        // If subsequent entry with this name and not array
1489        } else if (typeof hash[k] === "string") {
1490          hash[k] = v;  // replace it
1491        // modified to add support for objects
1492        } else if (typeof hash[k] === "object") {
1493          hash[k] = $.extend({}, hash[k], v);
1494        // If subsequent entry with this name and is array
1495        } else {
1496          hash[k].push(v);
1497        }
1498      }
1499      return hash;
1500    },
1501    refreshQueryString: function(urlString, data, settings) {
1502      var _this = this,
1503          queryString = urlString.split('?'),
1504          path = queryString.shift(),
1505          urlOptions;
1506
1507      urlOptions = this.deserialize(urlString);
1508
1509      // Loop through each dynatable param and update the URL with it
1510      for (attr in settings.params) {
1511        if (settings.params.hasOwnProperty(attr)) {
1512          var label = settings.params[attr];
1513          // Skip over parameters matching attributes for disabled features (i.e. leave them untouched),
1514          // because if the feature is turned off, then parameter name is a coincidence and it's unrelated to dynatable.
1515          if (
1516            (!settings.features.sort && attr == "sorts") ||
1517              (!settings.features.paginate && _this.anyMatch(attr, ["page", "perPage", "offset"], function(attr, param) { return attr == param; }))
1518          ) {
1519            continue;
1520          }
1521
1522          // Delete page and offset from url params if on page 1 (default)
1523          if ((attr === "page" || attr === "offset") && data["page"] === 1) {
1524            if (urlOptions[label]) {
1525              delete urlOptions[label];
1526            }
1527            continue;
1528          }
1529
1530          // Delete perPage from url params if default perPage value
1531          if (attr === "perPage" && data[label] == settings.dataset.perPageDefault) {
1532            if (urlOptions[label]) {
1533              delete urlOptions[label];
1534            }
1535            continue;
1536          }
1537
1538          // For queries, we're going to handle each possible query parameter individually here instead of
1539          // handling the entire queries object below, since we need to make sure that this is a query controlled by dynatable.
1540          if (attr == "queries" && data[label]) {
1541            var queries = settings.inputs.queries || [],
1542                inputQueries = $.makeArray(queries.map(function() { return $(this).attr('name') }));
1543
1544            if (settings.features.search) { inputQueries.push('search'); }
1545
1546            for (var i = 0, len = inputQueries.length; i < len; i++) {
1547              var attr = inputQueries[i];
1548              if (data[label][attr]) {
1549                if (typeof urlOptions[label] === 'undefined') { urlOptions[label] = {}; }
1550                urlOptions[label][attr] = data[label][attr];
1551              } else {
1552                delete urlOptions[label][attr];
1553              }
1554            }
1555            continue;
1556          }
1557
1558          // If we haven't returned true by now, then we actually want to update the parameter in the URL
1559          if (data[label]) {
1560            urlOptions[label] = data[label];
1561          } else {
1562            delete urlOptions[label];
1563          }
1564        }
1565      }
1566      return decodeURI($.param(urlOptions));
1567    },
1568    // Get array of keys from object
1569    // see http://stackoverflow.com/questions/208016/how-to-list-the-properties-of-a-javascript-object/208020#208020
1570    keysFromObject: function(obj){
1571      var keys = [];
1572      for (var key in obj){
1573        keys.push(key);
1574      }
1575      return keys;
1576    },
1577    // Find an object in an array of objects by attributes.
1578    // E.g. find object with {id: 'hi', name: 'there'} in an array of objects
1579    findObjectInArray: function(array, objectAttr) {
1580      var _this = this,
1581          foundObject;
1582      for (var i = 0, len = array.length; i < len; i++) {
1583        var item = array[i];
1584        // For each object in array, test to make sure all attributes in objectAttr match
1585        if (_this.allMatch(item, objectAttr, function(item, key, value) { return item[key] == value; })) {
1586          foundObject = item;
1587          break;
1588        }
1589      }
1590      return foundObject;
1591    },
1592    // Return true if supplied test function passes for ALL items in an array
1593    allMatch: function(item, arrayOrObject, test) {
1594      // start off with true result by default
1595      var match = true,
1596          isArray = $.isArray(arrayOrObject);
1597      // Loop through all items in array
1598      $.each(arrayOrObject, function(key, value) {
1599        var result = isArray ? test(item, value) : test(item, key, value);
1600        // If a single item tests false, go ahead and break the array by returning false
1601        // and return false as result,
1602        // otherwise, continue with next iteration in loop
1603        // (if we make it through all iterations without overriding match with fal
1603se,
1604        // then we can return the true result we started with by default)
1605        if (!result) { return match = false; }
1606      });
1607      return match;
1608    },
1609    // Return true if supplied test function passes for ANY items in an array
1610    anyMatch: function(item, arrayOrObject, test) {
1611      var match = false,
1612          isArray = $.isArray(arrayOrObject);
1613
1614      $.each(arrayOrObject, function(key, value) {
1615        var result = isArray ? test(item, value) : test(item, key, value);
1616        if (result) {
1617          // As soon as a match is found, set match to true, and return false to stop the `$.each` loop
1618          match = true;
1619          return false;
1620        }
1621      });
1622      return match;
1623    },
1624    // Return true if two objects are equal
1625    // (i.e. have the same attributes and attribute values)
1626    objectsEqual: function(a, b) {
1627      for (attr in a) {
1628        if (a.hasOwnProperty(attr)) {
1629          if (!b.hasOwnProperty(attr) || a[attr] !== b[attr]) {
1630            return false;
1631          }
1632        }
1633      }
1634      for (attr in b) {
1635        if (b.hasOwnProperty(attr) && !a.hasOwnProperty(attr)) {
1636          return false;
1637        }
1638      }
1639      return true;
1640    },
1641    // Taken from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/105074#105074
1642    randomHash: function() {
1643      return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
1644    }
1645  };
1646
1647  //-----------------------------------------------------------------
1648  // Build the dynatable plugin
1649  //-----------------------------------------------------------------
1650
1651  // Object.create support test, and fallback for browsers without it
1652  if ( typeof Object.create !== "function" ) {
1653    Object.create = function (o) {
1654      function F() {}
1655      F.prototype = o;
1656      return new F();
1657    };
1658  }
1659
1660  //-----------------------------------------------------------------
1661  // Global dynatable plugin setting defaults
1662  //-----------------------------------------------------------------
1663
1664  $.dynatableSetup = function(options) {
1665    defaults = mergeSettings(options);
1666  };
1667
1668  // Create dynatable plugin based on a defined object
1669  $.dynatable = function( object ) {
1670    $.fn['dynatable'] = function( options ) {
1671      return this.each(function() {
1672        if ( ! $.data( this, 'dynatable' ) ) {
1673          $.data( this, 'dynatable', Object.create(object).init(this, options) );
1674        }
1675      });
1676    };
1677  };
1678
1679  $.dynatable(dt);
1680
1681})(jQuery);
1682;
1683/* 
1684 * To change this license header, choose License Headers in Project Properties.
1685 * To change this template file, choose Tools | Templates
1686 * and open the template in the editor.
1687 */
1688
1689var currentDataSet = [];
1690var dynatable ;
1691jQuery(document).ready(function($){
1692
1693    /**
1694     * tabs
1695     */
1696    if($("#vpn-tabs").length){
1697        $("#vpn-tabs a").click(function(e){
1698            $("#vpn-tabs a.selected").removeClass("selected");
1699            $(this).addClass("selected");
1700            $(".tab-content.selected").removeClass("selected");
1701            $($(this).attr("href")).addClass("selected");
1702            e.preventDefault();
1703        });
1704    }
1705    
1706
1707    
1708    function resetForm(formid){
1709        $(':input',formid)
1710                .not(':button, :submit, :reset, :hidden, :radio')
1711                .removeAttr('checked')
1712                .removeAttr('selected');
1713    }
1714    
1715    $(".reset-form").click(function(e){
1716       e.preventDefault();
1717       formid = $(this).closest('form');
1718       resetForm("#" + formid.attr('id'));
1719       searchForVPNs();
1720       
1721    });
1722    /**
1723     * Stars
1724     * 
1725     * move and re-order the radios
1726     */
1727    if($(".star-cb-group").length){
1728        $(".form-item-rating .form-type-radio").each(function(index){
1729            $(".star-cb-group").prepend($(this).html());
1730        });
1731        $(".star-cb-group").before("<label>" + $(".form-item-rating > label:eq(0)").text() + "</label>");
1732        $(".form-item-rating").remove();
1733        var logID = 'log',
1734        log = $('<div id="' + logID + '"></div>');
1735        $('body').append(log);
1736        $('.star-cb-group [type*="radio"]').change(function() {
1737          var me = $(this);
1738          log.html(me.attr('value'));
1739        });
1740    }
1741        
1742    /**
1743     * Dynatable
1744     */
1745    
1746   if($("#table-vpn").length){
1747       top5filter = [];
1748       len = 5;
1749       if(vpnjson.length < 5){
1750           len = vpnjson.length;
1751       }
1752       for(i = 0; i < len ; i++ ){
1753           if (i === 5) { continue; }
1754           top5filter[i] = vpnjson[i];
1755       }
1756      
1757       currentDataSet = stripDataForDynatable(top5filter);
1758       setupDyantable();
1759   }
1760   
1761   $('#paopao-vpn-compare-openselectvpn-form').live('submit',  function(e){
1762       e.preventDefault();
1763       
1764       moveSearchFilters();
1765       
1766       resetForm($("#vpn-search-form").closest("form"));   
1767       
1768       currentDataSet = stripDataForDynatable(vpnjson);
1769       
1770       updateDynatable(currentDataSet);
1771       if($("#top-five-title").is(":visible")){
1772          $("#top-five-title").hide();
1773          $("#all-vpn-title").show();          
1774       }
1775       
1776       $("html, body").delay(2000).animate({ scrollTop: ($('#search-result-table').offset()) }, 1000);
1777       return false;
1778   });
1779   
1780   function moveSearchFilters(){
1781    if( $.contains($("#vpn-holder")[0],$("#vpn-search-form")[0]) ){
1782       $("#vpn-search-form").slideUp('slow', function(){
1783           $("#vpn-search-form").appendTo("#sidebar").end().slideDown('slow');
1784       });
1785   }
1786       
1787       
1788   }
1789   
1790   function setupDyantable(){
1791       if($(window).width() > 765){
1792          $("#table-vpn").show();
1793          $("#list-vpn").hide();
1794           dynatable = $("#table-vpn").dynatable({
1795                 features: {
1796                    paginate: false,
1797                    recordCount: false,
1798                    sorting: true,
1799                    search: false
1800                  },
1801                  dataset: {
1802                      records : currentDataSet
1803                  }
1804              }).data('dynatable');
1805        } else {
1806          $("#table-vpn").hide();
1807          $("#list-vpn").show();
1808          moveSearchFilters();
1809           dynatable = $("#list-vpn").dynatable({
1810                 features: {
1811                    paginate: false,
1812                    recordCount: false,
1813                    sorting: true,
1814                    search: false
1815                  },
1816                  dataset: {
1817                      records : currentDataSet
1818                  },
1819                  table: {
1820                    bodyRowSelector: 'li'
1821                  },
1822                  writers: {
1823                    _rowWriter: ulWriter
1824                  },
1825                  
1826              }).data('dynatable');
1827        }
1828   }
1829   
1830   
1831   $("#showmore-than-top5").click(function(e){
1832      e.preventDefault();
1833      
1834      tabledata = stripDataForDynatable(vpnjson);
1835      
1836      updateDynatable(tabledata);
1837      
1838      $("#showmore-than-top5, #top-five-title").hide();
1839   });
1840    
1841    
1842   $("#look-arround").click(function(e){
1843      e.preventDefault();
1844      moveSearchFilters();
1845       
1846      resetForm($("#vpn-search-form").closest("form"));
1847      
1848      tabledata = stripDataForDynatable(vpnjson);
1849      
1850      updateDynatable(tabledata);
1851      
1852      $("#showmore-than-top5, #top-five-title").hide(); 
1853   });  
1854$( ":checkbox" ).live('change',function(e) {
1855    if ($(this).attr('id') === "edit-about-device-device-type-desktop--2" || $(this).attr('id') === "edit-about-device-device-type-mobile--2" ){
1856        mob = 0;
1857        dsk = 0;
1858        if($("#edit-about-device-device-type-mobile--2").is(':checked')){
1859            mob = 1;
1860        }
1861        if($("#edit-about-device-device-type-desktop--2").is(':checked')){
1862            dsk = 1;
1863        }        
1864        if(mob === 0 && dsk === 0 ){
1865            $("#edit-os-type-desktop-type--2, #edit-os-type-mobile-type--2").show();
1866        }
1867        if(mob === 1 && dsk === 0 ){
1868            $("#edit-os-type-mobile-type--2").slideDown();
1869            $("#edit-os-type-desktop-type--2").slideUp();
1870            $("#edit-os-type-desktop-type--2 input").removeAttr('checked');
1871        }
1872        if(mob === 0 && dsk === 1 ){
1873            $("#edit-os-type-mobile-type--2").slideUp();
1874            $("#edit-os-type-desktop-type--2").slideDown();
1875            $("#edit-os-type-mobile-type--2 input").removeAttr('checked');
1876        }
1877        if(mob === 1 && dsk === 1 ){
1878            $("#edit-os-type-desktop-type--2, #edit-os-type-mobile-type--2").slideDown();
1879        }
1880    }
1881    
1882    if($(window).width() > 765){
1883        if( $.contains($("#sidebar")[0],$("#vpn-search-form")[0]) ){
1884            searchForVPNs();
1885        }
1886    }
1887});
1888    
1889    function updateDynatable(newRecords, sorted){
1890        
1891        dynatable.settings.dataset.originalRecords = newRecords;
1892        dynatable.process();
1893        if(typeof sorted === 'undefined'){
1894            dynatable.sorts.add('editors_rating', "-1");
1895        }        
1896        if(newRecords.length ===0){
1897            noResultPossible();
1898        }
1899        
1900    }
1901    
1902   function stripDataForDynatable(data){
1903       output = [];
1904       for(i = 0; i < data.length; i++ ){
1905         output[i] = {
1906                     title : data[i].display_heading,
1907                     users_rating  : data[i].users_average,
1908                     editors_rating : data[i].editors_average,
1909                     download : data[i].download,
1910                     security : data[i].shielding_result
1911                    };                  
1912       }
1913       return output;
1914   } 
1915   
1916   $(".open-close-filters").click(function(e){
1917       e.preventDefault();
1918       $(this).parent('div').find("div.fieldset-wrapper").toggle();
1919   })
1920   
1921   
1922   $("#vpn-search-submit").click(function(e){
1923       e.preventDefault();
1924       $("#top-five-title").hide();
1925       searchForVPNs();
1926       $("#showmore-than-top5").hide();
1927       moveSearchFilters();
1928   });
1929   
1930   function search(nameKey, term, myArray){
1931        var foundrows = [];
1932        for (var i=0; i < myArray.length; i++) {
1933            if (myArray[i][term] === nameKey) {
1934                foundrows.push(myArray[i]);
1935            }
1936        }
1937        return foundrows;
1938    }
1939    
1940    var combine = function(a, min) {
1941    var fn = function(n, src, got, all) {
1942        if (n == 0) {
1943            if (got.length > 0) {
1944                all[all.length] = got;
1945            }
1946            return;
1947        }
1948        for (var j = 0; j < src.length; j++) {
1949            fn(n - 1, src.slice(j + 1), got.concat([src[j]]), all);
1950        }
1951        return;
1952    }
1953    var all = [];
1954    for (var i = min; i < a.length; i++) {
1955        fn(i, a, [], all);
1956    }
1957    all.push(a);
1958    return all;
1959}
1960    
1961    function searchSubkey(nameKeyArr, subkey, myArray){
1962        var foundrows = [];
1963        var foudids = [];
1964        for (var i=0; i < myArray.length; i++) {
1965           
1966            for(var j=0; j < myArray[i][subkey].length;j++){
1967               
1968                var found = false;
1969                for(k=0; k<nameKeyArr.length;k++){
1970                    if (found !== true && myArray[i][subkey][j] === nameKeyArr[k] && foudids.indexOf(i) === -1 ) {
1971                        if(foundrows.hasOwnProperty(myArray[i])) {
1972                            continue;
1973                        }
1974                        foundrows.push(myArray[i]);
1975                        foudids.push(i);
1976                        found=true;
1977                    }
1978                }
1979            }
1980        }
1981        return foundrows;
1982    }
1983   function searchForVPNs(){
1984       formValues = {};
1985      
1986       $.each($('#paopao-vpn-compare-selectvpn-form--2').serializeArray(), function(_, kv) {
1987        formValues[kv.name] = kv.value;
1988      });
1989      
1990      workingset = vpnjson;
1991     
1992      /** search device types */
1993      if(formValues.hasOwnProperty("about_device[device_type][mobile]")&&formValues.hasOwnProperty("about_device[device_type][desktop]")){
1994          workingset = getDevice(workingset, [formValues['about_device[device_type][mobile]'],formValues['about_device[device_type][desktop]']]);
1995      } else if(formValues.hasOwnProperty("about_device[device_type][mobile]")){
1996          workingset = getDevice(workingset,[formValues['about_device[device_type][mobile]']]);
1997      } else if (formValues.hasOwnProperty("about_device[device_type][desktop]")){
1998          workingset = getDevice(workingset,[formValues['about_device[device_type][desktop]']]);
1999      }
2000      
2001      /** search browsers **/
2002      browserArrayCheck = combine (["firefox",'chrome','safari','opera','ie'],1).reverse();
2003     
2004      for(i=0;i<browserArrayCheck.length; i++ ){
2005          found = 0;
2006          var searchthings = [];
2007          for(j=0; j<browserArrayCheck[i].length;j++){
2008              if(formValues.hasOwnProperty("browser_type[browser]["+browserArrayCheck[i][j]+"]")){
2009                  found++;
2010                  
2011                  searchthings.push(formValues["browser_type[browser]["+browserArrayCheck[i][j]+"]"]);
2012              }
2013          }
2014          if(browserArrayCheck[i].length === found){
2015              workingset =  getBrowser(workingset, searchthings);
2016              break;
2017              
2018          }
2019      }   
2020      /** search desktop os **/
2021       osArrayCheck = combine (["osx",'windows','linux'],1).reverse();
2022      for(i=0;i<osArrayCheck.length; i++ ){
2023          found = 0;
2024          var searchthings = [];
2025          for(j=0; j<osArrayCheck[i].length;j++){
2026              if(formValues.hasOwnProperty("os_type[desktop_type]["+osArrayCheck[i][j]+"]")){
2027                  found++;
2028                  searchthings.push(formValues["os_type[desktop_type]["+osArrayCheck[i][j]+"]"]);
2029              }
2030          }
2031          if(osArrayCheck[i].length === found){
2032              workingset =  getOS(workingset, searchthings);
2033              break;
2034          }
2035      }  
2036      
2037      /** search mobile os **/
2038       osArrayCheck = combine (["ios",'android','windows_mobile','blackberry'],1).reverse();
2039      for(i=0;i<osArrayCheck.length; i++ ){
2040          found = 0;
2041          var searchthings = [];
2042          for(j=0; j<osArrayCheck[i].length;j++){
2043              if(formValues.hasOwnProperty("os_type[mobile_type]["+osArrayCheck[i][j]+"]")){
2044                  found++;
2045                  searchthings.push(formValues["os_type[mobile_type]["+osArrayCheck[i][j]+"]"]);
2046              }
2047          }
2048     
2049          if(osArrayCheck[i].length === found){
2050              workingset =  getOS(workingset, searchthings);
2051              break;
2052          }
2053      }  
2054      
2055      if(formValues.hasOwnProperty("encryption_type[security][none]")){
2056          workingset = getSecurity(workingset, [formValues["encryption_type[security][none]"]]);
2057      } else {
2058          /** search security os **/
2059            securityArrayCheck = combine (["encrypted","anonymous"],1).reverse();
2060        
2061           for(i=0;i<securityArrayCheck.length; i++ ){
2062               found = 0;
2063               var searchthings = [];
2064               for(j=0; j<securityArrayCheck[i].length;j++){
2065                   if(formValues.hasOwnProperty("encryption_type[security]["+securityArrayCheck[i][j]+"]")){
2066                       found++;
2067                       searchthings.push(formValues["encryption_type[security]["+securityArrayCheck[i][j]+"]"]);
2068                   }
2069               }
2070
2071               if(securityArrayCheck[i].length === found){
2072                   workingset =  getSecurity(workingset, searchthings);
2073                   break;
2074               }
2075           }  
2076      }
2077      
2078      /** search security os **/
2079            purchaseArrayCheck = combine ([1,2,3],1).reverse();
2080             
2081
2082           for(i=0;i<purchaseArrayCheck.length; i++ ){
2083               found = 0;
2084               var searchthings = [];
2085               for(j=0; j<purchaseArrayCheck[i].length;j++){
2086                   if(formValues.hasOwnProperty("free_paid[purchase]["+purchaseArrayCheck[i][j]+"]")){
2087                       found++;
2088                       searchthings.push(formValues["free_paid[purchase]["+purchaseArrayCheck[i][j]+"]"]);
2089                   }
2090               }
2091
2092               if(purchaseArrayCheck[i].length === found){
2093                   workingset =  getPayments(workingset, searchthings);
2094                   break;
2095               }
2096           }  
2097           
2098      tabledata = stripDataForDynatable(workingset);
2099      
2100      updateDynatable(tabledata);
2101   }
2102   
2103   function getDevice(workingset, device){
2104        
2105       return searchSubkey(device,"device_type", workingset);
2106   }
2107   
2108   function getBrowser(workingset, browser){
2109       return searchSubkey(browser,"browsers", workingset);
2110   }
2111   
2112    function getOS(workingset, os){
2113       return searchSubkey(os,"operating_system", workingset);
2114   }
2115   
2116     function getSecurity(workingset, enc){
2117       return searchSubkey(enc,"security", workingset);
2118   }
2119
2120   function getPayments(workingset, pay){
2121       
2122       return searchSubkey(pay,"purchase_options", workingset);
2123   }    
2124    
2125   function findUrl(searchText){
2126    var uri_pattern = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/ig;
2127     result= searchText.match(uri_pattern);
2128    if(result){return result;}else{return '';}
2129   }
2130    function ulWriter(rowIndex, record, columns, cellWriter) {
2131        var li;
2132           li = '<li class="overview-list-item">';
2133                //li += '<a href="'+findUrl(record.download)+'">';
2134                li += '<div class="overview-thumbnail">';
2135                    li += record.title ;
2136                li += '</div>';    
2137                    li += '<ul class="sub-set-info"><li class="first"><h3>'+ vpnlang.users_rating +'</h3>' + record.users_rating + '</li>';
2138                    li += '<li class="middle"><h3>'+ vpnlang.editors_rating +'</h3>' + record.editors_rating + '</li>';
2139                    li += '<li class="last"><h3>'+ vpnlang.security +'</h3>' + record.security + '</li></ul>';
2140                    li += '<div class="clean"></div>'; 
2141                    li += '<h3 class="vpn-button"><a href="'+findUrl(record.download)+'">'+ vpnlang.download +'</a></h3>';    
2142               // li += '</a>'
2143            li += '</li>';
2144        return li;
2145    }
2146
2147    $(window).resize(function(){
2148        if($("body.node-type-search-page").length){
2149            setupDyantable();
2150            if($(window).width() <= 765){
2151                moveSearchFilters();
2152                $("#block-block-1").hide();
2153            }
2154        }
2155    });
2156    
2157    $("#quick-search-field").keyup(function(e){
2158          if($(this).hasClass("noresultfound")){
2159             $(this).removeClass("noresultfound") 
2160          } 
2161        searchBoxTyping($(this).val().toLowerCase());
2162          
2163    });
2164    
2165//    $("#quick-search-field").click(function(e){
2166//      e.preventDefault();
2167//      if($(this).hasClass("noresultfound")){
2168//         $(this).removeClass("noresultfound") 
2169//      }
2170//       searchBoxTyping($("#edit-searchbytext").val().toLowerCase()); 
2171//    });
2172    
2173    $("#paopao-vpn-compare-searchvpn-form--2").submit(function(e){
2174       e.preventDefault(); 
2175    });
2176    
2177    function searchBoxTyping(text){
2178        if(text.length >=1){
2179            output = '';
2180            len = vpnjson.length;
2181            for(i=0;i<len;i++){
2182                if(JSON.stringify(vpnjson[i].title.toLowerCase()).indexOf(text)!== -1){
2183                    output += '<a href="'+findUrl(vpnjson[i].download)+'">'+vpnjson[i].title+'</a>';
2184                }
2185            }
2186            if(output.length > 0 ){
2187                $("#quick-search-results").empty().append(output);
2188            }else{
2189               $("#quick-search-results").empty(); 
2190               $("#edit-searchbytext").addClass("noresultfound"); 
2191            }
2192        }else{
2193            $("#quick-search-results").empty();
2194        }
2195            
2196    }
2197    
2198    function noResultPossible(){
2199        html = "<tr><td colspan='5'>"+noresultpossible+"<td></tr>";
2200        $("#table-vpn tbody").append(html);
2201    }
2202});;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.