FacilAuto Keys

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

  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.3
  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 == '#/';
  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-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. 'Enter': () => simulateClick(document.activeElement),
  112. 'J': focusNextTest,
  113. 'K': focusPreviousTest,
  114. // 'Tab': next button (implicit, they're tabbable elements)
  115. };
  116.  
  117. function isTargetPage() {
  118. const urlMatch = window.location.hash.startsWith('#/test/block/');
  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-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. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址