UCalgary Card & Collection Keyboard Shortcuts

Cards: number keys to answer; Enter/~/etc. Collections: number keys to Play a deck.

当前为 2025-05-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name UCalgary Card & Collection Keyboard Shortcuts
  3. // @version 3.1
  4. // @description Cards: number keys to answer; Enter/~/etc. Collections: number keys to Play a deck.
  5. // @match https://cards.ucalgary.ca/card/*
  6. // @match https://cards.ucalgary.ca/collection/*
  7. // @match https://cards.ucalgary.ca/collection*
  8. // @grant none
  9. // @namespace https://greasyfork.org/users/1331386
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. /* ─────────────── helper ─────────────── */
  16.  
  17. const isCardPage = location.pathname.startsWith('/card/');
  18. const isCollectionPage = location.pathname.startsWith('/collection/') && location.pathname !== '/collection';
  19. const isCollectionRootPage = location.pathname === '/collection'
  20.  
  21. // common 0-19 index from keyboard (Shift+1-0 for 10-19)
  22. function getOptionIndex(e) {
  23. const map = {
  24. Digit1: 0, Digit2: 1, Digit3: 2, Digit4: 3, Digit5: 4,
  25. Digit6: 5, Digit7: 6, Digit8: 7, Digit9: 8, Digit0: 9,
  26. };
  27. if (!(e.code in map)) return null;
  28. return map[e.code] + (e.shiftKey ? 10 : 0);
  29. }
  30.  
  31. /* ─────────────── /card/  logic ─────────────── */
  32.  
  33. function addCardHints() {
  34. document.querySelectorAll('form.question').forEach(form => {
  35. form.querySelectorAll('.option label').forEach((label, i) => {
  36. if (!label.dataset.hinted) {
  37. label.insertAdjacentHTML(
  38. 'afterbegin',
  39. `<span style="font-weight:600;margin-right:4px;">(${i + 1})</span>`
  40. );
  41. label.dataset.hinted = 'true';
  42. }
  43. });
  44. });
  45. }
  46.  
  47. function clearSelections() {
  48. document.querySelectorAll('form.question input[type="radio"], form.question input[type="checkbox"]')
  49. .forEach(inp => (inp.checked = false));
  50. }
  51.  
  52. function handleEnter() {
  53. const submitBtn = document.querySelector('form.question .submit button');
  54. const nextBtn = document.querySelector('#next');
  55. const reviewBtn = document.querySelector('div.actions span.review-buttons a.save');
  56.  
  57. if (submitBtn && submitBtn.offsetParent !== null) {
  58. submitBtn.click();
  59. } else if (nextBtn && nextBtn.offsetParent !== null) {
  60. nextBtn.click();
  61. } else if (reviewBtn && reviewBtn.offsetParent !== null) {
  62. reviewBtn.click();
  63. }
  64. }
  65. /* ─────────────── /collection/  logic ─────────────── */
  66.  
  67. function addCollectionHints() {
  68. document.querySelectorAll('table.table-striped tbody tr').forEach((row, i) => {
  69. const name = row.querySelector('a.deck-name');
  70. if (name && !name.dataset.hinted) {
  71. name.insertAdjacentHTML(
  72. 'afterbegin',
  73. `<span style="font-weight:600;margin-right:4px;">(${i + 1})</span>`
  74. );
  75. name.dataset.hinted = 'true';
  76. }
  77. });
  78. }
  79.  
  80. function playDeck(index) {
  81. const buttons = document.querySelectorAll('a.btn.action.save');
  82. const btn = buttons[index + 1];
  83. if (!btn) return;
  84.  
  85. btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
  86.  
  87. /* -------- open in a centred 1 000 × 800 px window -------- */
  88. const width = 1000;
  89. const height = 800;
  90. const left = Math.round((screen.width - width) / 2);
  91. const top = Math.round((screen.height - height) / 2);
  92.  
  93. const features = [
  94. 'noopener', // security: no access back to this window
  95. 'noreferrer', // (optional) hide referrer
  96. 'scrollbars=yes', // allow scrolling
  97. 'resizable=yes', // let user resize
  98. `width=${width}`,
  99. `height=${height}`,
  100. `left=${left}`,
  101. `top=${top}`
  102. ].join(',');
  103.  
  104. const win = window.open(btn.href, '_blank', features);
  105. if (win) win.focus(); // bring the new window to the front
  106. }
  107.  
  108.  
  109. /* ─── /collection root page ─ ENTER = first *visible* Details ─── */
  110. function openFirstBagDetails() {
  111. // .bag wrappers are what get “display:none” when filtered
  112. const bags = document.querySelectorAll('.bag');
  113.  
  114. for (const bag of bags) {
  115. // offsetParent === null → element (or an ancestor) is display:none
  116. if (bag.offsetParent === null) continue;
  117.  
  118. const detailsBtn = bag.querySelector('a.btn.deck-details');
  119. if (detailsBtn) {
  120. window.open(detailsBtn.href, '_blank', 'noopener');
  121. break;
  122. }
  123. }
  124. }
  125. /* ─────────────── key handler ─────────────── */
  126.  
  127. document.addEventListener('keydown', e => {
  128. const index = getOptionIndex(e);
  129. if (isCollectionRootPage && e.key === 'Enter') {
  130. openFirstBagDetails();
  131. e.preventDefault(); // stop the page’s default handling
  132. return;
  133. }
  134. if (isCardPage) {
  135. if (index !== null) {
  136. const radios = document.querySelectorAll('form.question input[type="radio"]');
  137. const checks = document.querySelectorAll('form.question input[type="checkbox"]');
  138.  
  139. if (radios[index]) {
  140. radios[index].checked = true;
  141. radios[index].scrollIntoView({ behavior: 'smooth', block: 'center' });
  142. }
  143. if (checks[index]) {
  144. checks[index].checked = !checks[index].checked;
  145. checks[index].scrollIntoView({ behavior: 'smooth', block: 'center' });
  146. }
  147. return;
  148. }
  149. if (e.key === 'Enter') handleEnter();
  150. if (e.key === '~') clearSelections();
  151. }
  152.  
  153. if (isCollectionPage && index !== null) {
  154. playDeck(index);
  155. }
  156. });
  157.  
  158. /* ─────────────── observers / init ─────────────── */
  159.  
  160. function initHints() {
  161. if (isCardPage) addCardHints();
  162. if (isCollectionPage) addCollectionHints();
  163. }
  164.  
  165. window.addEventListener('load', initHints);
  166. document.addEventListener('DOMContentLoaded', initHints);
  167. new MutationObserver(initHints).observe(document.body, { childList: true, subtree: true });
  168. })();