1// EasyChair source script 2// (c) 2010-2016 easychair.org 3// (c) 2018-2019,2023 EasyChair Ltd 4 5/** @typedef {{head?: HTMLElement, rows: HTMLElement[]}} Section */ 6 7function Tab(id,uniqueId,hasHeader,hasSummary,hashScroll) 8{ 9 try { 10 this.id = id; 11 /** Instead of array with rows we use array with sections. Every 12 * section is an array with rows. 13 * @type {Section[]} */ 14 this.sections = []; 15 this.numberOfRows = 0; 16 Tab.tables[id] = this; 17 this.navigation = false; // if true, has navigation bar 18 // this is the table state 19 this.inc = false; // if true, then sorting in an increasing order 20 this.currentColumn = -1; // current sort column 21 this.page = 1; 22 this.displayRows = 50; 23 this.search = ''; 24 this.drawing = false; 25 this.uniqueId = uniqueId; 26 this.hasHeader = hasHeader; 27 this.hasSummary = hasSummary; 28 this.hashScroll = hashScroll; 29 } 30 catch (err) { 31 logError(err,'Tab.new'); 32 } 33} // Tab 34 35Tab.tables = {}; 36 37Tab.init = function() 38{ 39 try { 40 Tab.findTables(); 41 var iter = values(Tab.tables).iterator(); 42 var hashScroll = false; 43 while (iter.hasNext()) { 44 var tab = iter.next(); 45 tab.initialize(); 46 tab.restoreState(); 47 hashScroll = hashScroll || tab.hashScroll; 48 } 49 Tab.initialized = true; 50 if (hashScroll) { 51 addEventHandler(window, 'hashchange', Tab.hashChanged); 52 // scroll element into view if the page hash contains an element name 53 Tab.hashChanged2(location.hash); 54 } 55 } 56 catch (err) { 57 logError(err,'Tab.init'); 58 } 59} // Tab.init 60 61/** 62 * Find all new kind tables on the page and process it. 63 */ 64Tab.findTables = function () { 65 try { 66 var tables = []; 67 var allTables = document.getElementsByTagName('table'); 68 for (var t = 0; t < allTables.length; ++t) { 69 var table = allTables[t]; 70 if (table.getAttribute('data-kind') === 'new') { 71 var id = table.id; 72 var uid = table.getAttribute('data-uid'); 73 var hasHeader = table.getAttribute('data-header') === 'true'; 74 var hasSummary = table.getAttribute('data-summary') === 'true'; 75 var hashScroll = table.getAttribute('data-scroll') === 'true'; 76 77 var sort = {}; 78 var headersColumn = table.getElementsByTagName('th'); 79 var header = null; 80 // need to find the header with sort columns. 81 for (var i = 0; i < headersColumn.length; ++i) { 82 var cell = headersColumn[i]; 83 if (cell.hasAttribute('data-sort')) { 84 header = cell.parentElement; 85 break; 86 } 87 } 88 89 // add listeners to found header 90 if (header) { 91 for (var i = 0; i < header.children.length; ++i) { 92 var column = header.children[i]; 93 var sortKind = column.getAttribute('data-sort'); 94 if (sortKind) { 95 sort[i + 1] = sortKind; 96 column.addEventListener('click', Tab.sortColumn.bind(null, id, i + 1)); 97 } 98 } 99 } 100 101 new Tab(id, uid, hasHeader, hasSummary, hashScroll).setSort(sort); 102 } 103 } 104 } catch (err) { 105 logError(err, 'Tab.findTables'); 106 } 107} // findTables 108 109Tab.saveAll = function() 110{ 111 try { 112 var iter = values(Tab.tables).iterator(); 113 while (iter.hasNext()) { 114 iter.next().saveState(); 115 } 116 } 117 catch (err) { 118 logError(err,'Tab.saveAll'); 119 } 120} // Tab.saveAll 121 122Tab.sortColumn = function(tableId,columnNumber) 123{ 124 try { 125 if (!Tab.initialized) { 126 Tab.notFullyLoaded(); 127 return; 128 } 129 Tab.tables[tableId].sortOn(columnNumber); 130 } 131 catch (err) { 132 logError(err,'Tab.sortColumn'); 133 } 134} // Tab.sortColumn 135 136Tab.notFullyLoaded = function() 137{ 138 alert('It seems that the table was not fully loaded, so this operation cannot be performed.'); 139} // Tab.notFullyLoaded 140 141Tab.prototype = { 142 initialize : function() 143 { 144 try { 145 var tab = $(this.id); 146 /** @type {HTMLCollection} */ 147 var rows = tab.rows; 148 if (!rows) { // browser bugs??? 149 return; 150 } 151 152 var entriesField = $(this.id + ':entries'); 153 if (entriesField) { 154 this.displayRows = EC.getSelection(entriesField); 155 if (this.displayRows !== 'all') { 156 this.displayRows = Number(this.displayRows); 157 } 158 this.navigation = true; 159 } 160 161 // add attributes for the primary sort 162 var startRow = this.navigation ? 1 : 0; 163 if (this.hasHeader) { 164 startRow++; 165 } 166 var endRow = rows.length; 167 if (this.hasSummary) { 168 endRow--; 169 } 170 /** @type {Section} */ 171 var section = { 172 rows: [] 173 }; 174 for (var n = startRow; n < endRow; n++) { 175 var row = rows[n]; 176 addDebug('init_n',n); 177 addDebug('init_r',row ? 1 : 0); 178 if (row.getAttribute('data-ignore') !== null) { 179 if (section.head || section.rows.length) { 180 this.sections.push(section); 181 section = { 182 rows: [] 183 }; 184 } 185 section.head = row; 186 continue; 187 } 188 section.rows.push(row); 189 this.numberOfRows++; 190 191 row['ec:prim'] = this.navigation ? n - 1 : n; 192 193 var cells = row.cells; 194 for (var c = cells.length - 1; c >= 0; c--) { 195 var cell = cells[c]; 196 // innerText works too long if we will do it every time, so we store 197 // the content of the cell 198 cell['ec:content'] = cell.innerText; 199 } 200 } 201 if (section.head || section.rows.length) { 202 this.sections.push(section); 203 } 204 this.redraw(); 205 } 206 catch (err) { 207 this.dumpDebug(); 208 logError(err,'Tab.initialize'); 209 } 210 }, // initialize 211 212 dumpDebug : function() 213 { 214 try { 215 addDebug('tableId',this.id); 216 addDebug('numberOfRows',this.numberOfRows); 217 addDebug('navigation',this.navigation); 218 addDebug('hasHeader',this.hasHeader); 219 addDebug('hasSummary',this.hasSummary); 220 addDebug('inc',this.inc); 221 addDebug('currentColumn',this.currentColumn); 222 addDebug('page',this.page); 223 addDebug('displayRows',this.displayRows); 224 addDebug('search',this.search); 225 addDebug('drawing',this.drawing); 226 addDebug('uniqueId',this.uniqueId); 227 } 228 catch (err) { 229 logError(err,'Tab.dumpDebug'); 230 } 231 }, // dumpDebug 232 233 setSort : function(sort) 234 { 235 this.sort = sort; 236 }, 237 238 /** 239 * Returns a table cell that need to compare for the currentColumn according 240 * colspan in the row. 241 * @param {HTMLTableRowElement}
241 row - Table row for comparign. 242 * @returns {HTMLTableCellElement} 243 */ 244 getComparableCell: function(row) { 245 var index = -1; 246 var column = this.currentColumn; 247 while (column > 0) { 248 index++; 249 column -= row.cells[index].colSpan; 250 } 251 return row.cells[index]; 252 }, // getComparableCell 253 254 /** 255 * Returns the index of the find row. If there are no this row, returns -1. 256 * @param {HTMLTableRowElement} row - Row in the table that need to find index. 257 * @returns {number} 258 */ 259 getRowIndex: function(row) { 260 var index = 0; 261 for (var k = 0; k < this.sections.length; ++k) { 262 var section = this.sections[k]; 263 for (var l = 0; l < section.rows.length; ++l) { 264 if (section.rows[l] === row) { 265 return index; 266 } 267 index++; 268 } 269 } 270 return -1; 271 }, 272 273 cellCompare: function(row1, row2) 274 { 275 try { 276 var currentColumn = this.currentColumn; 277 var sortKind = this.sort[currentColumn]; 278 var prim1 = row1['ec:prim']; 279 if (!prim1) { 280 return -1; 281 } 282 var prim2 = row2['ec:prim']; 283 if (!prim2) { 284 return 1; 285 } 286 287 var key1; 288 var key2; 289 if (sortKind === 'primary') { 290 key1 = prim1; 291 key2 = prim2; 292 } 293 else { 294 var cell1 = this.getComparableCell(row1); 295 var cell2 = this.getComparableCell(row2); 296 var text1 = cell1.getAttribute('data-key') || cell1['ec:content']; 297 var text2 = cell2.getAttribute('data-key') || cell2['ec:content']; 298 switch (sortKind) { 299 case 'bool': 300 key1 = Boolean(text1); 301 key2 = Boolean(text2); 302 break; 303 case 'int': 304 key1 = parseInt(text1); 305 key1 = isNaN(key1) ? -Infinity : key1; 306 key2 = parseInt(text2); 307 key2 = isNaN(key2) ? -Infinity : key2; 308 break; 309 case 'float': 310 case 'number': 311 key1 = parseFloat(text1); 312 key1 = isNaN(key1) ? -Infinity : key1; 313 key2 = parseFloat(text2); 314 key2 = isNaN(key2) ? -Infinity : key2; 315 break; 316 case 'string': 317 key1 = text1; 318 key2 = text2; 319 break; 320 default: 321 throw new Error('Incorrect sortKind'); 322 break; 323 } 324 } 325 326 if (key1 < key2) { 327 result = -1; 328 } 329 else if (key1 > key2) { 330 result = 1; 331 } 332 else if (prim1 < prim2) { 333 return -1; 334 } 335 else if (prim2 < prim1) { 336 return 1; 337 } 338 else { 339 return 0; 340 } 341 342 return this.inc ? result : -result; 343 } 344 catch (err) { 345 this.dumpDebug(); 346 logError(err,'Tab.cellCompare'); 347 } 348 }, // cellCompare 349 350 sortOn : function(column) 351 { 352 try { 353 if (this.drawing) { 354 return; 355 } 356 this.drawing = true; 357 this.page = 1; 358 var tab = $(this.id); 359 var rows = tab.rows; 360 var headerRowIndex = this.navigation ? 1 : 0; 361 var header = rows[headerRowIndex]; 362 if (this.currentColumn == column) { 363 this.inc = ! this.inc; 364 } 365 else { 366 var lastRowIndex = rows.length - 1; 367 if (this.hasSummary) { 368 lastRowIndex--; 369 } 370 if (this.currentColumn != -1) { 371 header.cells[this.currentColumn-1].className = 'sort_both'; 372 for (var i = 0; i < this.sections.length; ++i) { 373 for (var j = 0; j < this.sections[i].rows.length; ++j) { 374 removeClass(this.getComparableCell(this.sections[i].rows[j]), 375 'high'); 376 } 377 } 378 } 379 this.inc = false; 380 this.currentColumn = column; 381 for (var i = 0; i < this.sections.length; ++i) { 382 for (var j = 0; j < this.sections[i].rows.length; ++j) { 383 addClass(this.getComparableCell(this.sections[i].rows[j]), 'high'); 384 } 385 } 386 } 387 header.cells[column-1].className = this.inc ? 'sort_asc' : 'sort_desc'; 388 var myself = this; 389 for (var i = 0; i < this.sections.length; ++i) { 390 var sectionRows = this.sections[i].rows; 391 if (sectionRows.length === 0) { // empty section 392 continue; 393 } 394 sectionRows.sort(function(cellA, cellB) { 395 return myself.cellCompare(cellA, cellB); 396 }); 397 var tableSection = sectionRows[0].parentNode; 398 for (var n = sectionRows.length - 2; n >= 0; n--) { 399 var row1 = sectionRows[n]; 400 var row2 = sectionRows[n + 1]; 401 if (row1 && row2) { // added 23/07/2012 to prevent errors for pages incompletely loaded 402 tableSection.insertBefore(row1,row2); 403 } 404 } 405 } 406 this.redraw(); 407 this.drawing = false;
408 } 409 catch (err) { 410 this.dumpDebug(); 411 logError(err,'Tab.sortOn'); 412 } 413 }, // sortOn 414 415 /** 416 * Show the header if it exists for all sections from array. 417 * @param {Section[]} sections - Sections that need to show the header. 418 */ 419 showSectionsHeader: function(sections) { 420 for (var i = 0; i < sections.length; ++i) { 421 if (sections[i].head) { 422 removeClass(sections[i].head, 'hidden'); 423 } 424 } 425 }, // showSectionsHeader 426 427 // Redraw the table depending on the current state. This function assumes 428 // the rows are already sorted and simply checks which rows should be hidden 429 redraw : function() 430 { 431 try { 432 if (!this.navigation) { 433 return; 434 } 435 // the page should display eligible entries between first and last, counting from 1 436 var displayAll = this.displayRows == 'all'; 437 var last = displayAll ? this.numberOfRows : this.page * this.displayRows; 438 var first = displayAll ? 1 : last - this.displayRows + 1; 439 var eligible = 0; 440 /** Section without eligible elements. 441 * @type {Section[]} 442 */ 443 var noEligibleElements = []; 444 /* Logic of showing section headers: 445 We should show all section headers from the first section that have 446 visible elements to the last section that have visible elements. 447 There may be sections between the first and the last without 448 visible/eligible rows, we also should show it. 449 Plus all section headers before the first without eligible rows. 450 Plus all section headers after the last section if this is the last 451 page. */ 452 for (var i = 0; i < this.sections.length; ++i) { 453 var section = this.sections[i]; 454 var hasEligibleElements = false; 455 var hasVisibleElements = false; 456 var firstEligibleElementIsVisible = false; 457 if (section.head) { 458 addClass(section.head, 'hidden'); 459 } 460 for (var j = 0; j < section.rows.length; ++j) { 461 var row = section.rows[j]; 462 463 if (this.search && !Tab.matchRow(row, this.search)) { 464 addClass(row, 'hidden'); 465 continue; 466 } 467 468 eligible++; 469 if (eligible >= first && eligible <= last) { 470 removeClass(row, 'hidden'); 471 hasVisibleElements = true; 472 if (!hasEligibleElements) { 473 firstEligibleElementIsVisible = true; 474 } 475 } else { 476 addClass(row,'hidden'); 477 } 478 hasEligibleElements = true; 479 } 480 if (hasVisibleElements && section.head) { 481 removeClass(section.head, 'hidden'); 482 } 483 if (hasEligibleElements) { 484 if (firstEligibleElementIsVisible) { 485 this.showSectionsHeader(noEligibleElements); 486 } 487 noEligibleElements = []; 488 } else { 489 noEligibleElements.push(section); 490 } 491 } 492 // Or displayAll is true, or it is last page. 493 if (displayAll || this.page === Math.ceil(eligible / this.displayRows)) { 494 this.showSectionsHeader(noEligibleElements); 495 } 496 497 // change the "showing n entries" part of the bar 498 $(this.id + ':rows').innerHTML = eligible; 499 $(this.id + ':from').innerHTML = eligible < first ? 0 : first; 500 $(this.id + ':to').innerHTML = eligible < first ? 0 : eligible < last ? eligible : last; 501 // change the pages bar 502 $(this.id + ':previous').style.visibility = this.page > 1 ? 'visible' : 'hidden'; 503 $(this.id + ':next').style.visibility = eligible > last ? 'visible' : 'hidden'; 504 505 var rightLinks = displayAll ? 0 : Math.ceil((eligible - last) / this.displayRows); 506 var leftLinks = displayAll ? 0 : ((first - 1) / this.displayRows); 507 if (leftLinks + rightLinks > 8) { 508 if (leftLinks > 4) { 509 if (rightLinks >= 4) { 510 leftLinks = 4; 511 } 512 else { 513 leftLinks = 8 - rightLinks; 514 } 515 } 516 if (rightLinks > 4) { 517 if (leftLinks >= 4) { 518 rightLinks = 4; 519 } 520 else { 521 rightLinks = 8 - leftLinks; 522 } 523 } 524 } 525 var allHidden = displayAll || (first == 1 && eligible <= last); 526 var slices = Math.ceil(this.numberOfRows / 10); 527 var fromSlice = this.page - leftLinks; 528 var toSlice = this.page + rightLinks; 529 for (var n = slices; n > 0; n--) { 530 var selected = $(this.id + ':sel' + n);
531 var pageLink = $(this.id + ':page' + n); 532 if (allHidden || n < fromSlice || n > toSlice) { 533 addClass(selected,'hidden'); 534 addClass(pageLink,'hidden'); 535 } 536 else if (n == this.page) { 537 removeClass(selected,'hidden'); 538 addClass(pageLink,'hidden'); 539 } 540 else { 541 addClass(selected,'hidden'); 542 removeClass(pageLink,'hidden'); 543 } 544 } 545 } 546 catch (err) { 547 this.dumpDebug(); 548 logError(err,'Tab.redraw'); 549 } 550 }, // redraw 551 552 // function called when the user changes the number of displayed entries 553 changeEntries : function() 554 { 555 try { 556 if (!Tab.initialized) { 557 Tab.notFullyLoaded(); 558 return; 559 } 560 var field = $(this.id + ':entries'); 561 if (this.drawing) { 562 EC.setSelection(field,this.displayRows); 563 return; 564 } 565 this.drawing = true; 566 this.page = 1; 567 this.displayRows = EC.getSelection(field); 568 this.redraw(); 569 this.drawing = false; 570 } 571 catch (err) { 572 this.dumpDebug(); 573 logError(err,'Tab.changeEntries'); 574 } 575 }, // changeEntries 576 577 // function called when the user clicks on "next page" 578 nextPage : function() 579 { 580 try { 581 return this.setPage(this.page+1); 582 } 583 catch (err) { 584 logError(err,'Tab.nextPage'); 585 } 586 }, // nextPage 587 588 // function called when the user clicks on "previous page" 589 previousPage : function() 590 { 591 try { 592 return this.setPage(this.page-1); 593 } 594 catch (err) { 595 logError(err,'Tab.previousPage'); 596 } 597 }, // previousPage 598 599 // function called when the user clicks on a page number 600 // page numbers start from 1 601 setPage : function(page) 602 { 603 try { 604 if (!Tab.initialized) { 605 Tab.notFullyLoaded(); 606 return; 607 } 608 if (this.drawing) { 609 return false; 610 } 611 this.drawing = true; 612 this.page = page; 613 this.redraw(); 614 this.drawing = false; 615 return false; 616 } 617 catch (err) { 618 this.dumpDebug(); 619 logError(err,'Tab.setPage'); 620 } 621 }, // setPage 622 623 searchUpdate : function() 624 { 625 try { 626 if (!Tab.initialized) { 627 Tab.notFullyLoaded(); 628 return; 629 } 630 var searchString = $(this.id + ':search').value; 631 if (searchString == this.search) { 632 return; 633 } 634 if (this.drawing) { 635 // there should be rescheduling of this function call in future 636 return; 637 } 638 this.search = searchString; 639 this.drawing = true; 640 this.page = 1; 641 this.redraw(); 642 this.drawing = false; 643 } 644 catch (err) { 645 this.dumpDebug(); 646 logError(err,'Tab.searchUpdate'); 647 } 648 }, // searchUpdate 649 650 // save state in the session storage 651 saveState : function() 652 { 653 try { 654 saveObject(this.uniqueId, 655 {i : this.inc,c : this.currentColumn,p : this.page, 656 d : this.displayRows,s : this.search}); 657 } 658 catch (err) { 659 this.dumpDebug(); 660 logError(err,'Tab.saveState'); 661 } 662 }, 663 664 // restore state from the session storage 665 restoreState : function() 666 { 667 "use strict"; 668 try { 669 var uniqueId = this.uniqueId; 670 var state = getObject(uniqueId); 671 if (!state) { 672 return; 673 } 674 this.displayRows = state.d; 675 if (this.navigation) { 676 EC.setSelection($(this.id + ':entries'),state.d); 677 this.search = state.s; 678 $(this.id + ':search').value = state.s; 679 } 680 if (state.c != -1) { 681 this.sortOn(state.c); 682 // if the order is reverse, it should be sorted twice 683 if (state.i) { 684 this.sortOn(state.c); 685 } 686 } 687 this.page = state.p; 688 this.redraw(); 689 deleteObject(this.uniqueId); 690 } 691 catch (err) { 692 this.dumpDebug(); 693 logError(err,'Tab.restoreState'); 694 } 695 } 696}; // Tab.prototype 697 698Tab.matchRow = function(row,searchString) 699{ 700 try { 701 searchString = searchString.toLowerCase(); 702 var cells = row.cells; 703 for (var c = cells.length - 1; c >= 0; c--) { 704 if (cells[c]['ec:content'].toLowerCase().trim().indexOf(searchString) !== -1) { 705 return true; 706 } 707 } 708 return false;
709 } 710 catch (err) { 711 logError(err,'Tab.matchRow'); 712 } 713} // Tab.matchRow 714 715Tab.hashChanged = function(event) 716{ 717 try { 718 Tab.hashChanged2(event.newURL); 719 } 720 catch (err) { 721 logError(err,'Tab.hashChanged'); 722 } 723} // Tab.hashChanged 724 725Tab.hashChanged2 = function(URL) 726{ 727 try { 728 var hashIndex = URL.indexOf('#'); 729 if (hashIndex == -1) { 730 return; 731 } 732 var name = URL.substring(hashIndex + 1); 733 if (name == '') { 734 return; 735 } 736 737 // find if the element with this name is contained in a table row 738 var elements = document.getElementsByName(name); 739 for (var i = 0; i < elements.length; ++i) { 740 var tableRow = elements[i]; 741 while (tableRow && tableRow.tagName != 'TR') { 742 tableRow = tableRow.parentNode; 743 } 744 if (!tableRow) { 745 continue; 746 } 747 // tableRow is a <tr> element 748 // find a <table> element containing this row 749 var table = tableRow.parentNode; 750 while (table.tagName != 'TABLE') { 751 table = table.parentNode; 752 } 753 var tab = Tab.tables[table.id]; 754 if (!tab) { // table element but not one of our tables 755 continue; 756 } 757 if (!tab.hashScroll) { 758 // tab found but we should not scroll to it 759 return; 760 } 761 762 if (tab.search) { 763 $(tab.id + ':search').value = ''; 764 tab.search = ''; 765 } 766 767 var page = 1; 768 var rowsCount = parseInt(tab.displayRows); 769 if (rowsCount) { 770 var index = tab.getRowIndex(tableRow); 771 page = Math.floor(index / rowsCount) + 1; 772 } 773 tab.setPage(page); 774 tableRow.scrollIntoView(); 775 return; 776 } 777 } 778 catch (err) { 779 logError(err,'Tab.hashChanged2'); 780 } 781} // Tab.hashChanged2 782 783addOnLoad(Tab.init); 784addOnBeforeUnload(Tab.saveAll);
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.