1 /* (This is the new BSD license.) 2 * Copyright (c) 2014-2016, Chris Culy 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * * Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * * Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * * Neither the name of the Chris Culy nor the 13 * names of its contributors may be used to endorse or promote 14 * products from this software without specific prior written permission. 15 * 16 * THIS SOFTWARE IS PROVIDED BY Chris Culy 17 * ``AS IS'' AND ANY OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 18 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 * ARE DISCLAIMED. IN NO EVENT SHALL Chris Culy 20 * BE LIABLE FOR ANY, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 * CONSEQUENTIAL DAMAGES INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 22 * GOODS OR SERVICES; OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 * CAUSED AND ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 24 * TORT INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 25 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 */ 27 28 "use strict"; 29 /** 30 @namespace kwicIS 31 All of the functionality is in the kwicIS namespace 32 */ 33 var kwicIS = kwicIS || {}; 34 35 (function(){ 36 37 //the container is either an ID or the HTMl element 38 /** 39 * @class kwicIS.KWIC 40 * This is the class for the KWIC 41 * <p> 42 * The constructor 43 * @param container is the html element that will contain the KWIC. An id can be used instead. 44 */ 45 kwicIS.KWIC = function(container) { 46 var where; //the html element for the KWIC 47 var id; //the id of the element; we'll make up one if we need to 48 var data; // array of objects: {left: array of (TextInfo) leftContexts, hit:(TextInfo), right: array of (TextInfo) rightContexts, index:idx]} 49 var fromObjs = false; //is the input data objects (e.g. TextInfo), or delimited Strings 50 //if we have delimited strings as input, we need these 51 var fldNames = ["token"]; 52 var fldDelim = "/"; 53 var textInfoToString; //toString function for textInfo 54 var tmu = textmodel.TextModelUtilities(fldNames, fldDelim, textInfoToString); //for when we get delimited string input 55 var filters = {}; // {left: array of filter_functions, hit:filter_function, right: array of filter_functions. a filter_function gets an info object and returns Boolean. The function at index <em>i</em> filters position <em>i + 1</em> away from the root. Default is no filtering (via empty arrays) 56 var excludedIDs = d3.set(); //ids excluded by filters 57 // 58 var numHits; 59 var leftContextLen = 5; 60 var rtContextLen = 5; 61 var numToShow = 20; //number of hits to show at one time 62 var whichPage = 0; //which "page" of hits we are showing 63 var lastPage; //the last page; read only 64 var sortCol = 0; //i.e. the hit. For now just one column. 65 var headerData; 66 var headers; 67 var cellTitle = function(textInfo, i) {}; //noop 68 var colHeaderClick = function(headerContent, col) {}; //noop 69 var itemCellClick = function(textInfo, i) {}; //noop 70 var rowString = function(textInfoArray, i) {}; //noop 71 var rowStringOnRight; 72 var haveRowString = false; 73 var prefixesOnRight = false; 74 75 76 var kwic = this; //a way to keep track of the main object since functions each have their own _this_ 77 78 //we can pass either an ID or the actual HTML element this way 79 if (typeof(container) == "string") { 80 where = document.getElementById(container); 81 id = container; 82 } else { 83 where = container; // not ideal -- should check that this is really html element 84 id = container.id; 85 if (!id) { 86 container.id = "kwikContainer33"; 87 id = container.id; 88 } 89 } 90 91 var table = d3.select(where).append("table").attr("class", "kwicTable"); 92 table.append("thead").append("tr"); 93 table.append("tbody"); 94 95 setupHeaderData(); 96 97 //end of initialization in constructor 98 99 100 /////////// public functions 101 102 103 // setter/getters 104 105 /** 106 * Getter/setter for the data for the KWIC 107 * @param input is the data. It can be <strong>either</strong>: 108 * <p> 109 * an array of of arrays [leftContextsArray, hitsArray, rightContextsArray, indexArray], where the i-th elements of the arrays correspond 110 * and the contexts and hits are delimited strings (as defined by {@link kwicIS.KWIC#fieldNames} and {@link kwicIS.KWIC#fieldDelim} 111 * <p> 112 * <strong>or</strong> 113 * <p> 114 * an array of objects, where each object is of the form: {left: array of leftContexts, hits:element, right: array of rightContexts, index:element}, 115 * and all the elements of the contexts and the hits are themselves objects, typically with at least a "token" key. 116 * <p> 117 * If the input is delimited strings, they will be converted internally to #textmodel.TextInfo objects 118 * <p> 119 * If not specified, the current data is returned 120 */ 121 this.data = function(input) { 122 if (!arguments.length) { 123 return data; 124 } 125 126 //autodetect whether the input is strings or objects 127 if (typeof(input[0]) === "object" && input[0] instanceof Array) { //cf. http://javascript.crockford.com/remedial.html 128 data = []; 129 numHits = input[1].length; 130 for (var i=0;i<numHits;i++) { 131 132 if (! prefixesOnRight) { 133 data[i] = {'left': tmu.textArrayToTextInfo(input[0][i]), 134 'hit': new tmu.TextInfo(input[1][i]), 135 'right':tmu.textArrayToTextInfo(input[2][i]), 136 'index':input[3][i], 137 'id': i 138 }; 139 } else { 140 //need to swap left and right and reverse 141 var leftTI = tmu.textArrayToTextInfo(input[2][i]); 142 leftTI.reverse(); 143 var rtTI = tmu.textArrayToTextInfo(input[0][i]); 144 rtTI.reverse(); 145 data[i] = {'left': leftTI, 146 'hit': new tmu.TextInfo(input[1][i]), 147 'right':rtTI, 148 'index':input[3][i], 149 'id': i 150 }; 151 } 152 153 } 154 //data = realArray; 155 } else { 156 numHits = input.length; 157 //data = input; 158 data = input.map(function(d,i) { 159 d.id = i; 160 if (prefixesOnRight) { 161 //swap left and right and reverse 162 d.left.reverse(); 163 d.right.reverse(); 164 var tmp = d.left; 165 d.right = d.left; 166 d.left = tmp; 167 } 168 return d; 169 }); 170 } 171 172 filters = {}; 173 excludedIDs = d3.set(); 174 calcLastPage(); 175 return kwic; 176 } 177 178 /** 179 * @returns just the <em>ids</em> of the data that satisfies the current filters 180 */ 181 this.filteredIDs = function() { 182 //return data.filter(function(d) { 183 // return ! excludedIDs.has(d.id); 184 //}).map(function(d) {return d.id}); 185 186 var what = []; 187 for(var i=0,n=data.length;i<n;i++){ 188 var id = d.id; 189 if (! excludedIDs.has(d.id)) { 190 what.push(id); 191 } 192 } 193 return what; 194 } 195 196 /** 197 * Getter/setter for the filters 198 * @param value is an object: {left: array of filter_functions, hit:filter_function, right: array of filter_functions. 199 * A filter_function gets an info object and returns Boolean. The function at index <em>i</em> filters position <em>i + 1</em> away from the root. Default is no filtering (via empty arrays). 200 * <p> 201 * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that. 202 */ 203 this.filters = function(value) { 204 if (!arguments.length) { 205 return filters; 206 } 207 filters = value; 208 filter(); 209 return kwic; 210 } 211 212 /** 213 * Getter/setter for the field names when the input is delimited strings 214 * @param value is an array of strings — the field names 215 */ 216 this.fieldNames = function(value) { 217 if (!arguments.length) { 218 return fldNames; 219 } 220 fldNames = value; 221 tmu = textmodel.TextModelUtilities(fldNames, fldDelim, textInfoToString); 222 return kwic; 223 } 224 225 /** 226 * Getter/setter for the delimiter when the input is delimited strings 227 * @param value is a string 228 */ 229 this.fieldDelim = function(value) { 230 if (!arguments.length) { 231 return fldDelim; 232 } 233 fldDelim = value; 234 tmu = textmodel.TextModelUtilities(fldNames, fldDelim, textInfoToString); 235 return kwic; 236 } 237 238 /** 239 * Getter/setter for the toString function of TextInfor when the input is delimited strings 240 * @param value is a string 241 */ 242 this.textInfoToString = function(value) { 243 if (!arguments.length) { 244 return textInfoToString; 245 } 246 textInfoToString = value; 247 tmu = textmodel.TextModelUtilities(fldNames, fldDelim, textInfoToString); 248 return kwic; 249 } 250 251 /** 252 * Getter/setter for the number of results to show per page 253 * @param value is an integer 254 * <p> 255 * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that. 256 */ 257 this.numToShow = function(value) { 258 if (!arguments.length) { 259 return numToShow; 260 } 261 numToShow = value; 262 calcLastPage(); 263 return kwic; 264 } 265 266 /** 267 * Getter/setter for the number of the current page of results 268 * @param value is an integer 269 * <p> 270 * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that. 271 */ 272 this.whichPage = function(value) { 273 if (!arguments.length) { 274 return whichPage; 275 } 276 whichPage = value; 277 return kwic; 278 } 279 280 /** 281 * Getter/setter for the length of the left-hand context 282 * @param value is an integer 283 * <p> 284 * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that. 285 */ 286 this.leftContextLen = function(value) { 287 if (!arguments.length) { 288 return leftContextLen; 289 } 290 leftContextLen = value; 291 setupHeaderData(); 292 return kwic; 293 } 294 295 /** 296 * Getter/setter for the length of the right-hand context 297 * @param value is an integer 298 * <p> 299 * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that. 300 */ 301 this.rtContextLen = function(value) { 302 if (!arguments.length) { 303 return rtContextLen; 304 } 305 rtContextLen = value; 306 setupHeaderData(); 307 return kwic; 308 } 309 310 /** 311 * Convenience Getter/setter when both contexts are the same length 312 * @param value is an integer 313 * <p> 314 * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that. 315 */ 316 this.contextLen = function(value) { 317 if (!arguments.length) { 318 return rtContextLen; 319 } 320 leftContextLen = value; 321 rtContextLen = value; 322 setupHeaderData(); 323 return kwic; 324 } 325 326 /** 327 * Getter/setter for the script is right to left 328 * @param value is a boolean: true means the script is written right to left 329 * <p> 330 * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that. 331 */ 332 this.rtToLeft = function(value) { 333 if (!arguments.length) { 334 return prefixesOnRight; 335 } 336 prefixesOnRight = value; 337 return kwic; 338 } 339 340 341 /** 342 * @returns the number of hits for these (possibly filtered) results 343 */ 344 this.hitsCount = function() { 345 return numHits; 346 } 347 348 /** 349 * Returns the number of hits for these (possibly filtered) results, aggregated by the given field 350 * @param field is the field to use for the aggregation 351 * @returns a hash of the results 352 */ 353 this.hitsByField = function(field) { 354 //return hash of field's values and their counts 355 356 var what = {}; 357 358 data.filter(function(d) { 359 return ! excludedIDs.has(d.id); 360 }).forEach(function(d) { 361 var which = d.hit[field]; 362 if (typeof(what[which]) === 'undefined') { 363 what[which] = 0; 364 } 365 what[ which ]++; 366 }); 367 return what; 368 369 } 370 371 /** 372 * Returns the actual fields used in the item objects 373 * @returns a d3.set 374 */ 375 this.actualFields = function() { 376 if (typeof(data) != 'undefined' && data.length > 0) { 377 return d3.set(Object.keys(data[0].hit)); 378 } 379 return d3.set(); 380 } 381 382 /** 383 * Sets the function to be used for the mouse hover title 384 * @param theFun is a function(itemObject, i) and returns a string. <em>i</em> is the number <em>in the current page</em> 385 */ 386 this.cellTitle = function(theFun) { 387 cellTitle = theFun; 388 return kwic; 389 } 390 391 /** 392 * Sets the function to be called when the column header is clicked 393 * @param theFun is a function(headerContent, column) 394 */ 395 this.colHeaderClick = function(theFun) { 396 colHeaderClick = theFun; 397 return kwic; 398 } 399 400 /** 401 * Sets the function to be called when an item cell is clicked 402 * @param theFun is a function(itemObject, i). <em>i</em> is the number <em>in the current page</em> 403 * 404 */ 405 this.itemCellClick = function(theFun) { 406 itemCellClick = theFun; 407 return kwic; 408 } 409 410 //setter only 411 //theFun(textInfoArray, hitNumber) 412 /** 413 * Sets the function to display a string for the row in an extra column, and whether the column is on the right or left end of the row. Default is no extra string. 414 * @param theFun is a function(itemObjectArray, hitNumber) and returns a string 415 * @param onRight is <em>boolean</em> (not just truthy) for position of rowString — true is on the right and false is on the left. Default is on the right. 416 */ 417 this.rowString = function(theFun, onRight) { 418 rowString = theFun; 419 if (typeof(onRight) == "boolean") { 420 rowStringOnRight = onRight; 421 } else { 422 rowStringOnRight = true; //default 423 } 424 haveRowString = true; 425 setupHeaderData(); 426 return kwic; 427 } 428 429 430 //other functions 431 432 433 /** 434 * Sorts the data according to the given column and the relevant settings for field, direction, and order 435 * @param column the column to be sorted by 436 * @param settings optional object with keys: 437 * <ul> 438 * <li>field: the field to use as the sort key</li> 439 * <li>sortFun: function(a,b) where a and be are the values of <em>field</em> and returns -1,0,1 according to whether a is less than, equal to, or greater than b. 440 * Default is a case insensitive lexicographic sort 441 * </li> 442 * <li>reverse: true if the order of sortFun should be reversed. Default is false.</li> 443 * </ul> 444 */ 445 this.sort = function(column, settings) { 446 settings = settings || {}; 447 var field; 448 var reverse = false; 449 var sortFun; 450 451 if (settings.hasOwnProperty('field')) { 452 field = settings['field']; 453 } 454 if (settings.hasOwnProperty('reverse')) { 455 reverse = settings['reverse']; 456 } 457 if (settings.hasOwnProperty('sortFun')) { 458 sortFun = settings['sortFun']; 459 } 460 461 if (typeof(sortFun) === 'undefined') { 462 sortFun = function(a,b) { 463 var lowerA = a.toLocaleLowerCase(); 464 var lowerB = b.toLocaleLowerCase(); 465 //return lowerA < lowerB ? -1 : lowerA > lowerB ? 1 : 0; 466 return lowerA.localeCompare(lowerB); 467 } 468 } 469 470 471 var hitCol = leftContextLen +1; //user columns are 1 based 472 if (haveRowString && ! rowStringOnRight) { 473 column--; 474 } 475 data.sort(function(obj1, obj2) { 476 477 //get the relevant element 478 var elt1, elt2; 479 if (column == hitCol) { 480 elt1 = obj1['hit']; 481 elt2 = obj2['hit']; 482 } else if (column < hitCol) { 483 var which = column-1; 484 elt1 = obj1['left'][which]; 485 elt2 = obj2['left'][which]; 486 } else { 487 var which = column-(leftContextLen +2); //user columns are 1 based 488 elt1 = obj1['right'][which]; 489 elt2 = obj2['right'][which]; 490 } 491 492 //get the relevant string from the element 493 var comp1, comp2; 494 495 if (typeof(elt1) === 'undefined') { 496 comp1 = ""; 497 } else { 498 if (typeof(field) !== 'undefined') { 499 if (typeof(elt1[field]) === 'undefined') { 500 comp1 = ""; 501 } else { 502 comp1 = elt1[field]; 503 } 504 } else { 505 comp1 = elt1.toString(); 506 } 507 } 508 if (typeof(elt2) === 'undefined') { 509 comp2 = ""; 510 } else { 511 if (typeof(field) !== 'undefined') { 512 if (typeof(elt2[field]) === 'undefined') { 513 comp2 = ""; 514 } else { 515 comp2 = elt2[field]; 516 } 517 } else { 518 comp2 = elt2.toString(); 519 } 520 } 521 522 return sortFun(comp1, comp2); 523 524 }); 525 526 if (reverse) { 527 data.reverse(); 528 } 529 530 kwic.showPage(1); 531 } 532 533 //pageNum is 1-based for users, but internally we use 0-based 534 /** 535 * Show the given page of results 536 * @param pageNum is an integer. Page numbers are 1-based. 537 */ 538 this.showPage = function(pageNum) { 539 kwic.whichPage(pageNum -1).show(); 540 } 541 542 /** 543 * Show the next page of results 544 */ 545 this.showNextPage = function() { 546 if (whichPage == lastPage) { 547 return; 548 } 549 whichPage += 1; 550 kwic.show(); 551 } 552 553 /** 554 * Show the previous page of results 555 */ 556 this.showPrevPage = function() { 557 if (whichPage == 0) { 558 return; 559 } 560 kwic.whichPage( kwic.whichPage()-1 ).show(); 561 } 562 563 /** 564 * Show the first page of results 565 */ 566 this.showFirstPage = function() { 567 kwic.whichPage(0).show(); 568 } 569 570 /** 571 * Show the last page of results 572 */ 573 this.showLastPage = function() { 574 kwic.whichPage(lastPage).show(); 575 } 576 577 /** 578 * Show the current page of results. This is the function that does the work of showing the KWIC. 579 */ 580 this.show = function() { 581 582 //select items for this page, trimming left and rt context, return flattened array 583 var startElt = numToShow*whichPage; 584 var thisData = data.filter(function(d) { 585 return ! excludedIDs.has(d.id); 586 }) 587 .slice(startElt, startElt + numToShow).map(function(d, i){ 588 d.left = d.left.slice(-leftContextLen); 589 //pad if it is too short 590 while (d.left.length < leftContextLen) { 591 d.left.unshift(""); 592 } 593 d.right = d.right.slice(0, rtContextLen); 594 //pad if too short 595 while (d.right.length < rtContextLen) { 596 d.right.push(""); 597 } 598 599 var what = d.left.concat([d.hit], d.right); 600 var rowStr; 601 if (! prefixesOnRight) { 602 rowStr = [ rowString(what, startElt + i) ]; 603 } else { 604 var tmpWhat = what.slice(); 605 tmpWhat.reverse(); 606 rowStr = [ rowString(tmpWhat, startElt + i) ]; 607 } 608 609 610 611 //var rowStr = [ rowString(what, startElt + i) ]; 612 if (haveRowString) { 613 if (rowStringOnRight) { 614 what = what.concat(rowStr); 615 } else { 616 what = rowStr.concat(what); 617 } 618 } 619 return {'cells':what, 'index': d.index, 'id':d.id}; 620 621 }); 622 623 headers = d3.select(where).select(" table > thead > tr").selectAll("th") 624 .data(headerData); 625 626 headers.enter() 627 .append("th") 628 .text(String) 629 .classed("kwicTH", true) 630 .classed("kwicTHHit", function(d,i){ 631 return d == "HIT"; //from setupHeaderData 632 }) 633 .classed("kwicRowSummary", function(d,i) { 634 return isSummaryCol(i); 635 }) 636 .on("click", colHeaderClick); 637 638 639 /* https://github.com/d3/d3/blob/master/CHANGES.md#selections-d3-selection 640 var circle = svg.selectAll("circle").data(data) // UPDATE 641 .style("fill", "blue"); 642 643 circle.exit().remove(); // EXIT 644 645 circle.enter().append("circle") // ENTER 646 .style("fill", "green") 647 .merge(circle) // ENTER + UPDATE 648 .style("stroke", "black"); 649 650 */ 651 652 653 //tr 654 //UPDATE 655 var tr = d3.select(where).select(" table > tbody").selectAll("tr") 656 .data(thisData); 657 658 //EXIT 659 tr.exit().remove(); 660 661 var tre = tr.enter().append("tr") //ENTER 662 .merge(tr) //ENTER + UPDATE 663 .attr("index", function(d) { 664 return d.index; 665 }); 666 667 668 //td 669 //UPDATE 670 var td = tre.selectAll("td") 671 .data(function(d) { 672 return d.cells; 673 }); 674 675 td.enter().append("td") //ENTER 676 .on("click", itemCellClick) 677 .merge(td) //ENTER + UPDATE 678 //td.html(function(d) { 679 .html(function(d) { 680 return d.toString()}) 681 .attr("class","kwicTD") 682 .classed("kwicTDHit", function(d,i) { 683 var isHit; 684 if (!haveRowString || rowStringOnRight) { 685 isHit = (i == leftContextLen); 686 } else if (haveRowString) { 687 isHit = (i == (1+ leftContextLen)); 688 } 689 return isHit; 690 }) 691 .classed("kwicRowSummary", function(d,i) { 692 return isSummaryCol(i); 693 }) 694 .attr("title", cellTitle); 695 696 697 //pass the hit's id and index from the row to the cell 698 d3.select(where).selectAll('.kwicTDHit') 699 .each(function(d) { 700 var p = d3.select(this.parentNode); 701 702 d.id = +p.attr('id'); 703 d.index = p.attr('index'); 704 705 return d; 706 }); 707 708 709 //EXIT 710 //td.exit().remove(); //we don't need this since tr.exit() removes the whole row 711 712 return; 713 } 714 715 //convenience filter function factories 716 717 /** 718 * Convenience filter function factory for including items whose field is val 719 * @param field is the field to use 720 * @param val is the value to test 721 * @returns a function(itemObject) which returns true if itemObject[field] === val and false otherwise 722 */ 723 this.include_filter_function = function(field, val) { 724 return function(d) { 725 return d[field] === val; 726 } 727 } 728 729 //return a function that includes an item where the field does NOT have the value 730 /** 731 * Convenience filter function factory for excluding items whose field is val 732 * @param field is the field to use 733 * @param val is the value to test 734 * @returns a function(itemObject) which returns true if itemObject[field] !== val and false otherwise 735 */ 736 this.exclude_filter_function = function(field, val) { 737 return function(d) { 738 return d[field] !== val; 739 } 740 } 741 742 /////////// private functions 743 744 function setupHeaderData() { 745 //set up the headers 746 headerData = []; 747 for(var i=0;i<leftContextLen;i++) { 748 if (! prefixesOnRight) { 749 headerData.push("-" + (leftContextLen -i)) 750 } else { 751 headerData.push("+" + (leftContextLen -i)) 752 } 753 754 } 755 headerData.push("HIT"); 756 for(var i=0;i<rtContextLen;i++) { 757 if (!prefixesOnRight) { 758 headerData.push("+" + (1+i)) 759 } else { 760 headerData.push("-" + (1+i)) 761 } 762 763 } 764 if (haveRowString) { 765 if (rowStringOnRight) { 766 headerData.push("Summary"); 767 } else { 768 headerData.unshift("Summary"); 769 } 770 } 771 772 } 773 774 function isSummaryCol(i) { 775 if (haveRowString) { 776 if (rowStringOnRight) { 777 return i == headers.size()-1; 778 } else if (i == 0) { 779 return true; 780 } 781 } 782 return false; 783 } 784 785 function calcLastPage() { 786 numHits = data.length - excludedIDs.size(); 787 if (!data || numHits === 0) { 788 lastPage = 0; 789 } else { 790 lastPage = Math.floor((numHits-1) / numToShow) ; 791 } 792 } 793 794 function filter() { 795 excludedIDs = d3.set(); 796 797 798 var hitHasFilter = typeof(filters.hit) === 'function'; 799 var haveFilter = hitHasFilter; 800 801 var leftHasFilter = []; 802 for (var i=0, n=data[0].left.length; i<n; i++) { 803 leftHasFilter[i] = typeof(filters.left) !== 'undefined' && typeof(filters.left[i]) === 'function'; 804 haveFilter = haveFilter || leftHasFilter[i]; 805 } 806 807 var rightHasFilter = []; 808 for (var i=0, n=data[0].left.length; i<n; i++) { 809 rightHasFilter[i] = typeof(filters.right) !== 'undefined' && typeof(filters.right[i]) === 'function'; 810 haveFilter = haveFilter || rightHasFilter[i]; 811 } 812 813 //shortcut if we have no filters 814 if (! haveFilter) { 815 calcLastPage(); 816 return; 817 } 818 819 for(var i=0, n=data.length;i<n;i++) { 820 var d = data[i]; 821 822 if (hitHasFilter && ! filters.hit(d.hit)) { 823 excludedIDs.add(d.id); 824 continue; 825 } 826 827 var foundOne = false; 828 for(var j=0, m=d.left.length; j<m; j++) { 829 if (leftHasFilter[j] && ! filters.left[j](d.left[m-j-1])) { 830 excludedIDs.add(d.id); 831 foundOne = true; 832 break; 833 } 834 } 835 if (foundOne) { 836 continue; 837 } 838 839 for(var j=0, m=d.right.length; j<m; j++) { 840 if (rightHasFilter[j] && ! filters.right[j](d.right[j])) { 841 excludedIDs.add(d.id); 842 break; 843 } 844 } 845 846 } 847 calcLastPage(); 848 } 849 850 } 851 852 })()