app.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. /* global jQuery */
  2. /**
  3. * Tiny site-wide script. One purpose today: turn any element carrying a
  4. * `data-href` attribute into a clickable row / button without needing an
  5. * inline onclick (which would force `'unsafe-inline'` in the CSP).
  6. *
  7. * Middle-click / Ctrl+click / Cmd+click open in a new tab, matching
  8. * native anchor-link behaviour.
  9. */
  10. (function ($) {
  11. 'use strict';
  12. $(document).on('click', '[data-href]', function (ev) {
  13. // Ignore clicks that originated on an interactive descendant.
  14. if ($(ev.target).closest('a, button, input, select, textarea, label').length > 0) {
  15. return;
  16. }
  17. const href = String($(this).attr('data-href') || '');
  18. if (href === '') { return; }
  19. if (ev.metaKey || ev.ctrlKey || ev.button === 1) {
  20. window.open(href, '_blank');
  21. } else {
  22. window.location.href = href;
  23. }
  24. });
  25. // Make the row visibly clickable via keyboard + screen-readers.
  26. $('[data-href]').attr('role', 'link').attr('tabindex', '0');
  27. $(document).on('keydown', '[data-href]', function (ev) {
  28. if (ev.key === 'Enter') {
  29. ev.preventDefault();
  30. window.location.href = String($(this).attr('data-href') || '');
  31. }
  32. });
  33. })(jQuery);
  34. /**
  35. * Header hamburger menu. Vanilla JS — no jQuery. Toggles #app-menu's
  36. * [hidden] + aria-expanded on the trigger, closes on outside-click,
  37. * Escape (returning focus to the trigger), and on any menuitem click
  38. * (so following a link / submitting the sign-out form feels snappy).
  39. *
  40. * Pattern mirrors the owner-filter / columns dropdowns in
  41. * sprint-planner.js, minus jQuery.
  42. */
  43. (function () {
  44. 'use strict';
  45. const trigger = document.querySelector('[data-menu-trigger]');
  46. const menu = document.querySelector('[data-menu]');
  47. if (!trigger || !menu) { return; }
  48. function setOpen(open) {
  49. menu.hidden = !open;
  50. trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
  51. }
  52. trigger.addEventListener('click', function (ev) {
  53. ev.stopPropagation();
  54. setOpen(menu.hidden);
  55. });
  56. document.addEventListener('click', function (ev) {
  57. if (menu.hidden) { return; }
  58. if (ev.target === trigger || trigger.contains(ev.target)) { return; }
  59. if (!menu.contains(ev.target)) { setOpen(false); }
  60. });
  61. document.addEventListener('keydown', function (ev) {
  62. if (ev.key === 'Escape' && !menu.hidden) {
  63. setOpen(false);
  64. trigger.focus();
  65. }
  66. });
  67. menu.addEventListener('click', function (ev) {
  68. if (ev.target.closest('[role="menuitem"]')) { setOpen(false); }
  69. });
  70. })();
  71. /**
  72. * Phase 16: dark-mode toggle. theme-init.js in <head> already applied the
  73. * 'dark' class from localStorage before stylesheet resolution, so this
  74. * handler only cares about the on-click flip + the menu label's text.
  75. * localStorage writes are try/catch'd so private-window denials degrade
  76. * gracefully (toggle still works for the session, just doesn't persist).
  77. */
  78. (function () {
  79. 'use strict';
  80. const btn = document.querySelector('[data-theme-toggle]');
  81. const label = document.querySelector('[data-theme-label]');
  82. if (!btn || !label) { return; }
  83. function stamp() {
  84. label.textContent = document.documentElement.classList.contains('dark') ? 'Dark' : 'Light';
  85. }
  86. stamp();
  87. btn.addEventListener('click', function () {
  88. const nowDark = document.documentElement.classList.toggle('dark');
  89. try { localStorage.setItem('sp:theme', nowDark ? 'dark' : 'light'); } catch (e) { /* ignore */ }
  90. stamp();
  91. });
  92. })();