shortcuts.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. /**
  2. * @typedef {{ key: string, altKey?: boolean, ctrlKey?: boolean, metaKey?: boolean, shiftKey?: boolean }} KeyCombination
  3. * @typedef {{ code: string, altKey?: boolean, ctrlKey?: boolean, metaKey?: boolean, shiftKey?: boolean }} CodeCombination
  4. * @typedef {{ combination: KeyCombination, event: 'keydown' | 'keyup', callback: (evt: KeyboardEvent) => void, target: EventTarget }} RegisteredShortcut
  5. * @typedef {{ type?: 'keydown' | 'keyup', propagate?: boolean, disabledInInput?: boolean, target?: EventTarget }} ShortcutOptions
  6. */
  7. document.addEventListener('alpine:init', () => {
  8. Alpine.store('shortcuts', {
  9. /**
  10. * @type RegisteredShortcut[]
  11. */
  12. registeredShortcuts: [],
  13. /**
  14. * @param {KeyCombination | CodeCombination} combination
  15. * A combination using a `code` representing a physical key on the keyboard or a `key`
  16. * representing the character generated by pressing the key. Modifiers can be added using the
  17. * `altKey`, `ctrlKey`, `metaKey` or `shiftKey` parameters.
  18. * @param {(evt: KeyboardEvent) => void} callback
  19. * The callback function that will be called when the correct combination is pressed.
  20. * @param {ShortcutOptions?} options
  21. * An object of options, containing the event `type`, whether it will `propagate`, the `target`
  22. * element, and whether it's `disabledInInput`.
  23. * @returns {this} The `Shortcuts` object.
  24. */
  25. register(combination, callback, options) {
  26. /** @type ShortcutOptions */
  27. const defaultOptions = {
  28. type: 'keydown',
  29. propagate: false,
  30. disabledInInput: false,
  31. target: document,
  32. };
  33. options = { ...defaultOptions, ...options };
  34. /**
  35. * @param {KeyboardEvent} evt
  36. */
  37. const func = (evt) => {
  38. if (options.disabledInInput) {
  39. // Don't enable shortcut keys in input, textarea, select fields
  40. const element = evt.target.nodeType === 3 ? evt.target.parentNode : evt.target;
  41. if (['input', 'textarea', 'selectbox'].includes(element.tagName.toLowerCase())) {
  42. return;
  43. }
  44. }
  45. const validations = [
  46. combination.code
  47. ? combination.code == evt.code
  48. : combination.key.toLowerCase() == evt.key.toLowerCase(),
  49. (combination.altKey && evt.altKey) || (!combination.altKey && !evt.altKey),
  50. (combination.ctrlKey && evt.ctrlKey) || (!combination.ctrlKey && !evt.ctrlKey),
  51. (combination.metaKey && evt.metaKey) || (!combination.metaKey && !evt.metaKey),
  52. (combination.shiftKey && evt.shiftKey) || (!combination.shiftKey && !evt.shiftKey),
  53. ];
  54. const valid = validations.filter((validation) => validation);
  55. if (valid.length === validations.length) {
  56. callback(evt);
  57. if (!options.propagate) {
  58. evt.stopPropagation();
  59. evt.preventDefault();
  60. }
  61. }
  62. };
  63. this.registeredShortcuts.push({
  64. combination: combination,
  65. callback: func,
  66. target: options.target,
  67. event: options.type,
  68. });
  69. options.target.addEventListener(options.type, func);
  70. return this;
  71. },
  72. /**
  73. * @param {KeyCombination | CodeCombination} combination
  74. * A combination using a `code` representing a physical key on the keyboard or a `key`
  75. * representing the character generated by pressing the key. Modifiers can be added using the
  76. * `altKey`, `ctrlKey`, `metaKey` or `shiftKey` parameters.
  77. * @returns {this} The `Shortcuts` object.
  78. */
  79. unregister(combination) {
  80. const shortcut = this.registeredShortcuts.find(
  81. (shortcut) => JSON.stringify(shortcut.combination) == JSON.stringify(combination)
  82. );
  83. if (!shortcut) return;
  84. this.registeredShortcuts = this.registeredShortcuts.filter(
  85. (shortcut) => JSON.stringify(shortcut.combination) != JSON.stringify(combination)
  86. );
  87. shortcut.target.removeEventListener(shortcut.event, shortcut.callback, false);
  88. return this;
  89. },
  90. });
  91. Alpine.store('shortcuts')
  92. .register(
  93. { key: 'A' },
  94. (_evt) => {
  95. const createButton = document.querySelector('.button#btn-create');
  96. if (!createButton) {
  97. return;
  98. }
  99. location.href = createButton.href;
  100. },
  101. { disabledInInput: true }
  102. )
  103. .register(
  104. { key: 'A', ctrlKey: true, shiftKey: true },
  105. (_evt) => {
  106. const checked = document.querySelector('.l-unit .ch-toggle:eq(0)').checked;
  107. document
  108. .querySelectorAll('.l-unit')
  109. .forEach((el) => el.classList.toggle('selected'), !checked);
  110. document.querySelectorAll('.l-unit .ch-toggle').forEach((el) => (el.checked = !checked));
  111. },
  112. { disabledInInput: true }
  113. )
  114. .register({ code: 'Enter', ctrlKey: true }, (_evt) => {
  115. document.querySelector('form#vstobjects').submit();
  116. })
  117. .register({ code: 'Backspace', ctrlKey: true }, (_evt) => {
  118. const redirect = document.querySelector('a.button#btn-back').href;
  119. if (Alpine.store('form').dirty && redirect) {
  120. VE.helpers.createConfirmationDialog($('.js-confirm-dialog-redirect'), '', redirect);
  121. } else if (document.querySelector('form#vstobjects .button.cancel')) {
  122. location.href = $('form#vstobjects input.cancel')
  123. .attr('onclick')
  124. .replace("location.href='", '')
  125. .replace("'", '');
  126. } else if (redirect) {
  127. location.href = redirect;
  128. }
  129. })
  130. .register(
  131. { key: 'F' },
  132. (_evt) => {
  133. const searchBox = document.querySelector('.js-search-input');
  134. searchBox.focus();
  135. },
  136. { disabledInInput: true }
  137. )
  138. .register(
  139. { code: 'Digit1' },
  140. (_evt) => {
  141. const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(1) a');
  142. if (!target) {
  143. return;
  144. }
  145. if (Alpine.store('form').dirty) {
  146. VE.helpers.createConfirmationDialog($('.js-confirm-dialog-redirect'), '', target.href);
  147. } else {
  148. location.href = target.href;
  149. }
  150. },
  151. { disabledInInput: true }
  152. )
  153. .register(
  154. { code: 'Digit2' },
  155. (_evt) => {
  156. const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(2) a');
  157. if (!target) {
  158. return;
  159. }
  160. if (Alpine.store('form').dirty) {
  161. VE.helpers.createConfirmationDialog($('.js-confirm-dialog-redirect'), '', target.href);
  162. } else {
  163. location.href = target.href;
  164. }
  165. },
  166. { disabledInInput: true }
  167. )
  168. .register(
  169. { code: 'Digit3' },
  170. (_evt) => {
  171. const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(3) a');
  172. if (!target) {
  173. return;
  174. }
  175. if (Alpine.store('form').dirty) {
  176. VE.helpers.createConfirmationDialog($('.js-confirm-dialog-redirect'), '', target.href);
  177. } else {
  178. location.href = target.href;
  179. }
  180. },
  181. { disabledInInput: true }
  182. )
  183. .register(
  184. { code: 'Digit4' },
  185. (_evt) => {
  186. const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(4) a');
  187. if (!target) {
  188. return;
  189. }
  190. if (Alpine.store('form').dirty) {
  191. VE.helpers.createConfirmationDialog($('.js-confirm-dialog-redirect'), '', target.href);
  192. } else {
  193. location.href = target.href;
  194. }
  195. },
  196. { disabledInInput: true }
  197. )
  198. .register(
  199. { code: 'Digit5' },
  200. (_evt) => {
  201. const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(5) a');
  202. if (!target) {
  203. return;
  204. }
  205. if (Alpine.store('form').dirty) {
  206. VE.helpers.createConfirmationDialog($('.js-confirm-dialog-redirect'), '', target.href);
  207. } else {
  208. location.href = target.href;
  209. }
  210. },
  211. { disabledInInput: true }
  212. )
  213. .register(
  214. { code: 'Digit6' },
  215. (_evt) => {
  216. const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(6) a');
  217. if (!target) {
  218. return;
  219. }
  220. if (Alpine.store('form').dirty) {
  221. VE.helpers.createConfirmationDialog($('.js-confirm-dialog-redirect'), '', target.href);
  222. } else {
  223. location.href = target.href;
  224. }
  225. },
  226. { disabledInInput: true }
  227. )
  228. .register(
  229. { code: 'Digit7' },
  230. (_evt) => {
  231. const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(7) a');
  232. if (!target) {
  233. return;
  234. }
  235. if (Alpine.store('form').dirty) {
  236. VE.helpers.createConfirmationDialog($('.js-confirm-dialog-redirect'), '', target.href);
  237. } else {
  238. location.href = target.href;
  239. }
  240. },
  241. { disabledInInput: true }
  242. )
  243. .register(
  244. { key: 'H' },
  245. (_evt) => {
  246. const shortcutsDialog = document.querySelector('.shortcuts');
  247. if (shortcutsDialog.open) {
  248. shortcutsDialog.close();
  249. } else {
  250. shortcutsDialog.showModal();
  251. }
  252. },
  253. { disabledInInput: true }
  254. )
  255. .register({ code: 'Escape' }, (_evt) => {
  256. const shortcutsDialog = document.querySelector('.shortcuts');
  257. if (shortcutsDialog.open) {
  258. shortcutsDialog.close();
  259. }
  260. document.querySelectorAll('input, checkbox, textarea, select').forEach((el) => el.blur());
  261. })
  262. .register(
  263. { code: 'ArrowLeft' },
  264. (_evt) => {
  265. VE.navigation.move_focus_left();
  266. },
  267. { disabledInInput: true }
  268. )
  269. .register(
  270. { code: 'ArrowRight' },
  271. (_evt) => {
  272. VE.navigation.move_focus_right();
  273. },
  274. { disabledInInput: true }
  275. )
  276. .register(
  277. { code: 'ArrowDown' },
  278. (_evt) => {
  279. VE.navigation.move_focus_down();
  280. },
  281. { disabledInInput: true }
  282. )
  283. .register(
  284. { code: 'ArrowUp' },
  285. (_evt) => {
  286. VE.navigation.move_focus_up();
  287. },
  288. { disabledInInput: true }
  289. )
  290. .register(
  291. { key: 'L' },
  292. (_evt) => {
  293. const el = $('.units.active .l-unit.focus .shortcut-l');
  294. if (el.length) {
  295. VE.navigation.shortcut(el);
  296. }
  297. },
  298. { disabledInInput: true }
  299. )
  300. .register(
  301. { key: 'S' },
  302. (_evt) => {
  303. const el = $('.units.active .l-unit.focus .shortcut-s');
  304. if (el.length) {
  305. VE.navigation.shortcut(el);
  306. }
  307. },
  308. { disabledInInput: true }
  309. )
  310. .register(
  311. { key: 'W' },
  312. (_evt) => {
  313. const el = $('.units.active .l-unit.focus .shortcut-w');
  314. if (el.length) {
  315. VE.navigation.shortcut(el);
  316. }
  317. },
  318. { disabledInInput: true }
  319. )
  320. .register(
  321. { key: 'D' },
  322. (_evt) => {
  323. const el = $('.units.active .l-unit.focus .shortcut-d');
  324. if (el.length) {
  325. VE.navigation.shortcut(el);
  326. }
  327. },
  328. { disabledInInput: true }
  329. )
  330. .register(
  331. { key: 'R' },
  332. (_evt) => {
  333. const el = $('.units.active .l-unit.focus .shortcut-r');
  334. if (el.length) {
  335. VE.navigation.shortcut(el);
  336. }
  337. },
  338. { disabledInInput: true }
  339. )
  340. .register(
  341. { key: 'N' },
  342. (_evt) => {
  343. const el = $('.units.active .l-unit.focus .shortcut-n');
  344. if (el.length) {
  345. VE.navigation.shortcut(el);
  346. }
  347. },
  348. { disabledInInput: true }
  349. )
  350. .register(
  351. { key: 'U' },
  352. (_evt) => {
  353. const el = $('.units.active .l-unit.focus .shortcut-u');
  354. if (el.length) {
  355. VE.navigation.shortcut(el);
  356. }
  357. },
  358. { disabledInInput: true }
  359. )
  360. .register(
  361. { code: 'Delete' },
  362. (_evt) => {
  363. const el = $('.units.active .l-unit.focus .shortcut-delete');
  364. if (el.length) {
  365. VE.navigation.shortcut(el);
  366. }
  367. },
  368. { disabledInInput: true }
  369. )
  370. .register(
  371. { code: 'Enter' },
  372. (evt) => {
  373. if (evt.target.tagName == 'INPUT' && evt.target.form.id == 'vstobjects') {
  374. document.querySelector('form#vstobjects').submit();
  375. }
  376. if (Alpine.store('form').dirty) {
  377. if (!$('.ui-dialog').is(':visible')) {
  378. VE.helpers.createConfirmationDialog(
  379. $('.js-confirm-dialog-redirect')[0],
  380. '',
  381. document.querySelector(`${VE.navigation.state.menu_selector}.focus a`).href
  382. );
  383. } else {
  384. // if dialog is opened - submitting confirm box by "enter" shortcut
  385. document.querySelector('.ui-dialog button.submit').click();
  386. }
  387. } else {
  388. if (!$('.ui-dialog').is(':visible')) {
  389. const el = $('.units.active .l-unit.focus .shortcut-enter');
  390. if (el.length) {
  391. VE.navigation.shortcut(el);
  392. } else {
  393. VE.navigation.enter_focused();
  394. }
  395. } else {
  396. // if dialog is opened - submitting confirm box by "enter" shortcut
  397. document.querySelector('.ui-dialog button.submit').click();
  398. }
  399. }
  400. },
  401. { propagate: true }
  402. );
  403. });