RedditHomeChromeStyle.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import Foundation
  2. enum RedditHomeChromeStyle {
  3. /// Keeps Reddit header separators flush with the in-app sidebar divider and
  4. /// fixes Log In / menu hit targets that get covered by borders or feed chrome.
  5. static let injectionScript = """
  6. (function() {
  7. const STYLE_ID = 'reddora-home-chrome-style';
  8. if (window.__reddoraHomeChromeInstalled) return;
  9. window.__reddoraHomeChromeInstalled = true;
  10. const HEADER_ACTION_IDS = ['login-button', 'expand-user-drawer-button'];
  11. const DECORATIVE_SELECTORS = [
  12. 'faceplate-border',
  13. 'hr',
  14. '[role="separator"]',
  15. 'shreddit-overlay-display'
  16. ].join(', ');
  17. const style = document.createElement('style');
  18. style.id = STYLE_ID;
  19. style.textContent = `
  20. html,
  21. body,
  22. shreddit-app {
  23. margin-top: 0 !important;
  24. padding-top: 0 !important;
  25. }
  26. header,
  27. header > * {
  28. margin-left: 0 !important;
  29. }
  30. header {
  31. position: sticky !important;
  32. top: 0 !important;
  33. z-index: 300 !important;
  34. align-items: center !important;
  35. isolation: isolate !important;
  36. margin-top: 0 !important;
  37. padding-top: 0 !important;
  38. background: var(--shreddit-content-background, #ffffff) !important;
  39. }
  40. header faceplate-border,
  41. header [role="separator"],
  42. header hr {
  43. width: 100% !important;
  44. max-width: none !important;
  45. margin-left: 0 !important;
  46. margin-right: 0 !important;
  47. pointer-events: none !important;
  48. z-index: 0 !important;
  49. }
  50. header faceplate-search-input,
  51. header #SearchDropdown,
  52. header [data-testid="search-input-container"] {
  53. flex: 1 1 auto !important;
  54. min-width: 0 !important;
  55. max-width: 100% !important;
  56. overflow: hidden !important;
  57. align-self: center !important;
  58. pointer-events: auto !important;
  59. }
  60. /* Wrappers around header actions often extend past the visible button. */
  61. faceplate-tracker:has(#login-button),
  62. faceplate-tracker:has(#expand-user-drawer-button),
  63. rpl-tooltip:has(#login-button),
  64. rpl-tooltip:has(#expand-user-drawer-button),
  65. activate-feature:has(#expand-user-drawer-button) {
  66. display: contents !important;
  67. }
  68. #login-button,
  69. #expand-user-drawer-button {
  70. position: relative !important;
  71. z-index: 301 !important;
  72. pointer-events: auto !important;
  73. flex-shrink: 0 !important;
  74. align-self: center !important;
  75. touch-action: manipulation !important;
  76. }
  77. shreddit-overlay-display {
  78. pointer-events: none !important;
  79. }
  80. shreddit-overlay-display:has(
  81. shreddit-signup-drawer,
  82. auth-flow-modal,
  83. input[type="password"]
  84. ) {
  85. pointer-events: auto !important;
  86. }
  87. /* Feed sort row sits below the header; keep it from overlapping action buttons. */
  88. shreddit-sort-dropdown,
  89. [data-testid="sort-dropdown"],
  90. [data-testid="sort-posts-dropdown"],
  91. shreddit-feed-sort-dropdown {
  92. position: relative !important;
  93. z-index: 1 !important;
  94. }
  95. `;
  96. (document.head || document.documentElement).appendChild(style);
  97. function isDecorative(node) {
  98. if (!(node instanceof Element)) return false;
  99. return node.matches(DECORATIVE_SELECTORS)
  100. || (node.closest('header') && node.matches('faceplate-border, hr, [role="separator"]'));
  101. }
  102. function isInteractive(node) {
  103. if (!(node instanceof Element)) return false;
  104. return node.matches(
  105. 'button, a, [role="button"], input, select, textarea, label, faceplate-button'
  106. );
  107. }
  108. function clearBlockersForButton(button) {
  109. if (!(button instanceof Element)) return;
  110. const rect = button.getBoundingClientRect();
  111. if (rect.width < 1 || rect.height < 1) return;
  112. const sampleYs = [
  113. rect.top + rect.height * 0.25,
  114. rect.top + rect.height * 0.5,
  115. rect.top + rect.height * 0.75,
  116. rect.bottom - 1
  117. ];
  118. const x = rect.left + rect.width * 0.5;
  119. sampleYs.forEach(function(y) {
  120. const stack = document.elementsFromPoint(x, y);
  121. for (let i = 0; i < stack.length; i++) {
  122. const el = stack[i];
  123. if (el === button || button.contains(el)) break;
  124. if (isInteractive(el)) break;
  125. if (isDecorative(el) || !el.closest('header')) {
  126. el.style.setProperty('pointer-events', 'none', 'important');
  127. el.dataset.reddoraPointerFix = '1';
  128. }
  129. }
  130. });
  131. }
  132. function pinHeader() {
  133. document.querySelectorAll('header, reddit-header-large, faceplate-header').forEach(function(node) {
  134. if (!(node instanceof HTMLElement)) return;
  135. node.style.setProperty('position', 'sticky', 'important');
  136. node.style.setProperty('top', '0', 'important');
  137. node.style.setProperty('z-index', '300', 'important');
  138. node.style.setProperty(
  139. 'background',
  140. 'var(--shreddit-content-background, #ffffff)',
  141. 'important'
  142. );
  143. });
  144. }
  145. function patchHeaderHitTargets() {
  146. pinHeader();
  147. HEADER_ACTION_IDS.forEach(function(id) {
  148. const button = document.getElementById(id);
  149. if (!button) return;
  150. clearBlockersForButton(button);
  151. button.style.setProperty('position', 'relative', 'important');
  152. button.style.setProperty('z-index', '301', 'important');
  153. });
  154. document.querySelectorAll('header faceplate-border, header hr, header [role="separator"]').forEach(function(el) {
  155. el.style.setProperty('pointer-events', 'none', 'important');
  156. });
  157. }
  158. let patchTimer = null;
  159. function schedulePatch() {
  160. if (patchTimer) return;
  161. patchTimer = setTimeout(function() {
  162. patchTimer = null;
  163. patchHeaderHitTargets();
  164. }, 50);
  165. }
  166. patchHeaderHitTargets();
  167. [0, 100, 300, 750, 1500, 3000].forEach(function(delay) {
  168. setTimeout(patchHeaderHitTargets, delay);
  169. });
  170. const observer = new MutationObserver(schedulePatch);
  171. observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
  172. window.addEventListener('resize', schedulePatch, { passive: true });
  173. window.addEventListener('scroll', schedulePatch, { passive: true });
  174. function forwardClicksWithinButtonBounds(event) {
  175. if (event.type !== 'pointerdown') return;
  176. const menuButton = document.getElementById('expand-user-drawer-button');
  177. if (!menuButton || menuButton.contains(event.target)) return;
  178. const rect = menuButton.getBoundingClientRect();
  179. const x = event.clientX;
  180. const y = event.clientY;
  181. if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) return;
  182. event.preventDefault();
  183. event.stopImmediatePropagation();
  184. menuButton.click();
  185. }
  186. document.addEventListener('pointerdown', forwardClicksWithinButtonBounds, true);
  187. })();
  188. """
  189. }