RedditHomeChromeStyle.swift 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. /* Only suppress inert overlay layers that block header clicks. */
  78. shreddit-overlay-display:not(:has(
  79. button,
  80. a,
  81. [role="button"],
  82. [role="menu"],
  83. [role="menuitem"],
  84. [role="dialog"],
  85. [role="listbox"],
  86. faceplate-menu,
  87. input:not([type="hidden"]),
  88. textarea
  89. )) {
  90. pointer-events: none !important;
  91. }
  92. /* Feed sort row sits below the header; keep it from overlapping action buttons. */
  93. shreddit-sort-dropdown,
  94. [data-testid="sort-dropdown"],
  95. [data-testid="sort-posts-dropdown"],
  96. shreddit-feed-sort-dropdown {
  97. position: relative !important;
  98. z-index: 1 !important;
  99. }
  100. `;
  101. (document.head || document.documentElement).appendChild(style);
  102. function isDecorative(node) {
  103. if (!(node instanceof Element)) return false;
  104. return node.matches(DECORATIVE_SELECTORS)
  105. || (node.closest('header') && node.matches('faceplate-border, hr, [role="separator"]'));
  106. }
  107. function isInteractive(node) {
  108. if (!(node instanceof Element)) return false;
  109. return node.matches(
  110. 'button, a, [role="button"], input, select, textarea, label, faceplate-button'
  111. );
  112. }
  113. function clearBlockersForButton(button) {
  114. if (!(button instanceof Element)) return;
  115. const rect = button.getBoundingClientRect();
  116. if (rect.width < 1 || rect.height < 1) return;
  117. const sampleYs = [
  118. rect.top + rect.height * 0.25,
  119. rect.top + rect.height * 0.5,
  120. rect.top + rect.height * 0.75,
  121. rect.bottom - 1
  122. ];
  123. const x = rect.left + rect.width * 0.5;
  124. sampleYs.forEach(function(y) {
  125. const stack = document.elementsFromPoint(x, y);
  126. for (let i = 0; i < stack.length; i++) {
  127. const el = stack[i];
  128. if (el === button || button.contains(el)) break;
  129. if (isInteractive(el)) break;
  130. if (isDecorative(el) || !el.closest('header')) {
  131. el.style.setProperty('pointer-events', 'none', 'important');
  132. el.dataset.reddoraPointerFix = '1';
  133. }
  134. }
  135. });
  136. }
  137. function pinHeader() {
  138. document.querySelectorAll('header, reddit-header-large, faceplate-header').forEach(function(node) {
  139. if (!(node instanceof HTMLElement)) return;
  140. node.style.setProperty('position', 'sticky', 'important');
  141. node.style.setProperty('top', '0', 'important');
  142. node.style.setProperty('z-index', '300', 'important');
  143. node.style.setProperty(
  144. 'background',
  145. 'var(--shreddit-content-background, #ffffff)',
  146. 'important'
  147. );
  148. });
  149. }
  150. function patchHeaderHitTargets() {
  151. pinHeader();
  152. HEADER_ACTION_IDS.forEach(function(id) {
  153. const button = document.getElementById(id);
  154. if (!button) return;
  155. clearBlockersForButton(button);
  156. button.style.setProperty('position', 'relative', 'important');
  157. button.style.setProperty('z-index', '301', 'important');
  158. });
  159. document.querySelectorAll('header faceplate-border, header hr, header [role="separator"]').forEach(function(el) {
  160. el.style.setProperty('pointer-events', 'none', 'important');
  161. });
  162. }
  163. let patchTimer = null;
  164. function schedulePatch() {
  165. if (patchTimer) return;
  166. patchTimer = setTimeout(function() {
  167. patchTimer = null;
  168. patchHeaderHitTargets();
  169. }, 50);
  170. }
  171. patchHeaderHitTargets();
  172. [0, 100, 300, 750, 1500, 3000].forEach(function(delay) {
  173. setTimeout(patchHeaderHitTargets, delay);
  174. });
  175. const observer = new MutationObserver(schedulePatch);
  176. observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
  177. window.addEventListener('resize', schedulePatch, { passive: true });
  178. window.addEventListener('scroll', schedulePatch, { passive: true });
  179. function forwardClicksWithinButtonBounds(event) {
  180. if (event.type !== 'pointerdown') return;
  181. const menuButton = document.getElementById('expand-user-drawer-button');
  182. if (!menuButton || menuButton.contains(event.target)) return;
  183. const rect = menuButton.getBoundingClientRect();
  184. const x = event.clientX;
  185. const y = event.clientY;
  186. if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) return;
  187. event.preventDefault();
  188. event.stopImmediatePropagation();
  189. menuButton.click();
  190. }
  191. document.addEventListener('pointerdown', forwardClicksWithinButtonBounds, true);
  192. })();
  193. """
  194. }