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