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'> ▲</span>"); 1008 }; 1009 1010 this.appendArrowDown = function($link) { 1011 this.removeArrow($link); 1012 $link.append("<span class='dynatable-arrow'> ▼</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">…</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.