FacilAuto Keys

Añade navegación por teclado en algunas páginas de la aplicación web de FacilAuto (test, selección de test).

当前为 2025-03-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name FacilAuto Keys
  3. // @namespace victor-gp.dev
  4. // @description Añade navegación por teclado en algunas páginas de la aplicación web de FacilAuto (test, selección de test).
  5. // @match https://alumno.examentrafico.com/
  6. // @grant none
  7. // @author victor-gp
  8. // @license MIT
  9. // @homepageURL https://github.com/victor-gp/userscripts-facilauto-keybindings
  10. // @supportURL https://github.com/victor-gp/userscripts-facilauto-keybindings/issues
  11. // @contributionURL
  12. // @version 1.0.1
  13. // ==/UserScript==
  14.  
  15. (function testKeys() {
  16. 'use strict';
  17.  
  18. const script_id = 'facilauto-test-keys'
  19.  
  20. // Configuration: Map keys to CSS selectors
  21. const keySelectorMap = {
  22. 'A': 'img[src="/static/img/test/A.jpg"]',
  23. 'S': 'img[src="/static/img/test/B.jpg"]',
  24. 'D': 'img[src="/static/img/test/C.jpg"]',
  25. 'J': 'img[src="/static/img/test/back.png"]',
  26. 'K': 'img[src="/static/img/test/next.png"]',
  27. 'L': 'img[src="/static/img/test/end.png"]',
  28. 'W': 'button.help-button-1', // Ayuda
  29. 'E': 'button:has(svg.fa-images)', // Lamina
  30. 'R': 'button:has(svg.fa-volume-down)', // Audioexplicacion
  31. 'T': 'button:has(svg.fa-play)', // Videoexplicacion
  32. 'Enter': '.sweet-modal.is-visible button.btn-default', // Modal - White button
  33. 'Backspace': '.sweet-modal.is-visible button.btn-danger', // Modal - Red button
  34. };
  35.  
  36. // Configuration: Map keys to functions
  37. const keyFunctionMap = {
  38. 'Q': () => simulateClick(document.elementFromPoint(0, 0)), // Exit modal
  39. };
  40.  
  41. function isTargetPage() {
  42. const urlMatch = window.location.hash !== '#/test/block/test/exam/174/0';
  43. if (!urlMatch) return false;
  44. const contentMatch = document.querySelector('div.test-box-top') !== null;
  45. return contentMatch;
  46. }
  47.  
  48. function handleKeydown(event) {
  49. let key = event.key;
  50. // normalize letter keys
  51. if (/^[A-Za-z]$/.test(key)) {
  52. key = key.toUpperCase();
  53. }
  54.  
  55. if (keyFunctionMap[key]) {
  56. keyFunctionMap[key]();
  57. }
  58. else if (keySelectorMap[key]) {
  59. const element = document.querySelector(keySelectorMap[key]);
  60. simulateClick(element);
  61. }
  62. };
  63.  
  64. function simulateClick(element) {
  65. if (element) {
  66. element.click();
  67. }
  68. }
  69.  
  70. function handlePageChange() {
  71. if (isTargetPage()) {
  72. document.addEventListener('keydown', handleKeydown);
  73. console.debug(`${script_id}: load`);
  74. } else {
  75. document.removeEventListener('keydown', handleKeydown);
  76. console.debug(`${script_id}: remove`);
  77. }
  78. }
  79.  
  80. let lastUrl = window.location.href;
  81. function checkUrlChange() {
  82. const currentUrl = window.location.href;
  83. if (currentUrl !== lastUrl) {
  84. lastUrl = currentUrl;
  85. handlePageChange();
  86. }
  87. }
  88.  
  89. handlePageChange();
  90.  
  91. const observer = new MutationObserver(checkUrlChange);
  92. const config = { childList: true, subtree: true };
  93. observer.observe(document.body, config);
  94. })();
  95.  
  96. (function blockKeys() {
  97. "use strict";
  98.  
  99. const script_id = "facilauto-block-keys";
  100.  
  101. // Configuration: Map keys to CSS selectors _to focus_
  102. const keySelectorMap = {
  103. 'H': 'div.tests-block-item > .fail ~ .has-tooltip', // First failed test
  104. 'L': 'div.tests-block-item > div:first-child:not(.fail):not(.success) ~ .has-tooltip', // First not-taken test
  105. };
  106.  
  107. // Configuration: Map keys to functions
  108. const keyFunctionMap = {
  109. 'A': makeButtonsTabbable,
  110. 'Enter': () => simulateClick(document.activeElement),
  111. 'J': focusNextTest,
  112. 'K': focusPreviousTest,
  113. // 'Tab': next button (implicit)
  114. };
  115.  
  116. function isTargetPage() {
  117. const urlHashRegex = new RegExp("^#/test/block/");
  118. const urlMatch = window.location.hash.match(urlHashRegex);
  119. if (!urlMatch) return false;
  120. const contentMatch = document.querySelector("div.tests-index") !== null;
  121. return contentMatch;
  122. }
  123.  
  124. let tabbableElements;
  125. function makeButtonsTabbable() {
  126. // heuristic: elements with a tooltip seem to be buttons
  127. const tooltipElements = document.querySelectorAll(".has-tooltip");
  128. tabbableElements = Array.from(tooltipElements).filter(el => el.checkVisibility());
  129. tabbableElements.forEach((element) => {
  130. element.setAttribute("tabindex", "0");
  131. // element.style.border = "1px solid red";
  132. });
  133. tabbableElements[0].focus();
  134. }
  135.  
  136. function focusNextTest() {
  137. const currentIndex = tabbableElements.indexOf(document.activeElement);
  138. if (currentIndex !== -1) {
  139. tabbableElements[currentIndex + 4]?.focus();
  140. } else {
  141. tabbableElements[0].focus();
  142. }
  143. document.activeElement.scrollIntoView({ behavior: 'smooth', block: "center" });
  144. }
  145.  
  146. function focusPreviousTest() {
  147. const currentIndex = tabbableElements.indexOf(document.activeElement);
  148. if (currentIndex !== -1) {
  149. tabbableElements[currentIndex - 4]?.focus();
  150. } else {
  151. tabbableElements[0].focus();
  152. }
  153. document.activeElement.scrollIntoView({ behavior: 'smooth', block: "center" });
  154. }
  155.  
  156. function handleKeydown(event) {
  157. let key = event.key;
  158. // normalize letter keys
  159. if (/^[A-Za-z]$/.test(key)) {
  160. key = key.toUpperCase();
  161. }
  162.  
  163. if (keyFunctionMap[key]) {
  164. keyFunctionMap[key]();
  165. } else if (keySelectorMap[key]) {
  166. document.querySelector(keySelectorMap[key]).focus();
  167. }
  168. }
  169.  
  170. function simulateClick(element) {
  171. if (element) {
  172. element.click();
  173. }
  174. }
  175.  
  176. function waitUntil(condFn, execFn) {
  177. setTimeout(() => {
  178. if (condFn()) {
  179. execFn();
  180. } else {
  181. waitUntil(condFn, execFn)
  182. }
  183. }, 50)
  184. }
  185.  
  186. function handlePageChange() {
  187. if (isTargetPage()) {
  188. const isPageLoaded = () => {
  189. const tooltipElements = document.querySelectorAll(".has-tooltip");
  190. return tooltipElements.length !== 0;
  191. };
  192. const setUp = () => {
  193. makeButtonsTabbable();
  194. document.addEventListener("keydown", handleKeydown);
  195. console.debug(`${script_id}: load`);
  196. };
  197. waitUntil(isPageLoaded, setUp);
  198. } else {
  199. document.removeEventListener("keydown", handleKeydown);
  200. console.debug(`${script_id}: remove`);
  201. }
  202. }
  203.  
  204. let lastUrl = window.location.href;
  205. function checkUrlChange() {
  206. const currentUrl = window.location.href;
  207. if (currentUrl !== lastUrl) {
  208. lastUrl = currentUrl;
  209. handlePageChange();
  210. }
  211. }
  212.  
  213. handlePageChange();
  214.  
  215. const observer = new MutationObserver(checkUrlChange);
  216. const config = { childList: true, subtree: true };
  217. observer.observe(document.body, config);
  218. })();