|
224 | 224 | visibility: visible; |
225 | 225 | opacity: 1; |
226 | 226 | } |
| 227 | + |
| 228 | + /* Sortable columns */ |
| 229 | + .sortable { |
| 230 | + cursor: pointer; |
| 231 | + user-select: none; |
| 232 | + } |
| 233 | + |
| 234 | + .sortable:hover { |
| 235 | + text-decoration: underline; |
| 236 | + } |
227 | 237 | </style> |
228 | 238 | <script type="application/javascript"> |
229 | 239 | const grantsById = {}; |
|
415 | 425 | {% endfor %} |
416 | 426 |
|
417 | 427 | }; |
| 428 | + |
| 429 | + // Sorting functionality |
| 430 | + const sortTable = (sortBy, direction) => { |
| 431 | + const tbody = document.querySelector('#result_list tbody'); |
| 432 | + const rows = Array.from(tbody.querySelectorAll('tr.grant-item')); |
| 433 | + |
| 434 | + rows.sort((a, b) => { |
| 435 | + let aVal, bVal; |
| 436 | + |
| 437 | + if (sortBy === 'score') { |
| 438 | + aVal = parseFloat(a.dataset.score); |
| 439 | + bVal = parseFloat(b.dataset.score); |
| 440 | + } else if (sortBy === 'std_dev') { |
| 441 | + aVal = parseFloat(a.dataset.stdDev); |
| 442 | + bVal = parseFloat(b.dataset.stdDev); |
| 443 | + } |
| 444 | + |
| 445 | + // Handle null/NaN values - push them to the end |
| 446 | + if (isNaN(aVal)) aVal = direction === 'desc' ? -Infinity : Infinity; |
| 447 | + if (isNaN(bVal)) bVal = direction === 'desc' ? -Infinity : Infinity; |
| 448 | + |
| 449 | + if (direction === 'desc') { |
| 450 | + return bVal - aVal; |
| 451 | + } else { |
| 452 | + return aVal - bVal; |
| 453 | + } |
| 454 | + }); |
| 455 | + |
| 456 | + // Re-append rows in sorted order and update row numbers |
| 457 | + rows.forEach((row, index) => { |
| 458 | + row.querySelector('td:first-child').textContent = index + 1; |
| 459 | + tbody.appendChild(row); |
| 460 | + }); |
| 461 | + }; |
| 462 | + |
| 463 | + const initSorting = () => { |
| 464 | + document.querySelectorAll('.sortable').forEach(header => { |
| 465 | + header.addEventListener('click', (e) => { |
| 466 | + e.stopPropagation(); |
| 467 | + const sortBy = header.dataset.sort; |
| 468 | + let currentDir = header.dataset.sortDir; |
| 469 | + |
| 470 | + // Toggle direction |
| 471 | + let newDir; |
| 472 | + if (currentDir === 'desc') { |
| 473 | + newDir = 'asc'; |
| 474 | + } else { |
| 475 | + newDir = 'desc'; |
| 476 | + } |
| 477 | + |
| 478 | + // Update all sortable headers |
| 479 | + document.querySelectorAll('.sortable').forEach(h => { |
| 480 | + h.dataset.sortDir = ''; |
| 481 | + // Remove arrow from text, keeping tooltip if present |
| 482 | + const tooltipSpan = h.querySelector('.tooltiptext'); |
| 483 | + if (tooltipSpan) { |
| 484 | + h.childNodes[0].textContent = h.childNodes[0].textContent.replace(/ [▲▼]/, ''); |
| 485 | + } else { |
| 486 | + h.textContent = h.textContent.replace(/ [▲▼]/, ''); |
| 487 | + } |
| 488 | + }); |
| 489 | + |
| 490 | + // Set new direction and arrow |
| 491 | + header.dataset.sortDir = newDir; |
| 492 | + const arrow = newDir === 'desc' ? ' ▼' : ' ▲'; |
| 493 | + const tooltipSpan = header.querySelector('.tooltiptext'); |
| 494 | + if (tooltipSpan) { |
| 495 | + header.childNodes[0].textContent = header.childNodes[0].textContent.trim() + arrow; |
| 496 | + } else { |
| 497 | + header.textContent = header.textContent.trim() + arrow; |
| 498 | + } |
| 499 | + |
| 500 | + sortTable(sortBy, newDir); |
| 501 | + }); |
| 502 | + }); |
| 503 | + }; |
| 504 | + |
| 505 | + window.addEventListener('load', initSorting); |
418 | 506 | </script> |
419 | 507 | <ul class="object-tools"> |
420 | 508 | <li> |
@@ -471,13 +559,13 @@ <h3> |
471 | 559 | </th> |
472 | 560 | <th scope="col"> |
473 | 561 | <div class="text"> |
474 | | - <a>Score</a> |
| 562 | + <span class="sortable" data-sort="score" data-sort-dir="desc">Score ▼</span> |
475 | 563 | </div> |
476 | 564 | <div class="clear"></div> |
477 | 565 | </th> |
478 | 566 | <th scope="col"> |
479 | 567 | <div class="text"> |
480 | | - <span class="tooltip">Std Dev |
| 568 | + <span class="tooltip sortable" data-sort="std_dev" data-sort-dir="">Std Dev |
481 | 569 | <span class="tooltiptext">Standard Deviation: measures reviewer disagreement. High value = controversial (needs discussion), Low value = consensus.</span> |
482 | 570 | </span> |
483 | 571 | </div> |
|
532 | 620 | id="grant-{{ item.id }}" |
533 | 621 | data-type="{{ item.type }}" |
534 | 622 | data-num-of-votes="{{ item.userreview_set.count }}" |
| 623 | + data-score="{{ item.score|default_if_none:'-999' }}" |
| 624 | + data-std-dev="{{ item.std_dev|default_if_none:'-1' }}" |
535 | 625 | > |
536 | 626 | <td>{{ forloop.counter }}</td> |
537 | 627 | <td class="results-item"> |
|
0 commit comments