GitHub Project Filter Formatter (Responsive Button Inside)

Adds a responsive button inside and to the right of GitHub project filter input to format text as title:*XX*.

  1. // ==UserScript==
  2. // @name GitHub Project Filter Formatter (Responsive Button Inside)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.6
  5. // @description Adds a responsive button inside and to the right of GitHub project filter input to format text as title:*XX*.
  6. // @author xiaohaoxing
  7. // @match https://github.com/orgs/.*/projects/.*
  8. // @match https://github.com/*/*/projects/*
  9. // @grant none
  10. // @icon https://github.githubassets.com/favicons/favicon.svg
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const INPUT_ID = 'filter-bar-component-input';
  18. const BUTTON_ID = 'tampermonkey-format-title-button';
  19. const BUTTON_TEXT = '模糊';
  20. const WRAPPER_CLASS = 'tampermonkey-input-wrapper';
  21. const MIN_INPUT_TEXT_AREA_WIDTH = 50; // Minimum pixels for text before button (if button shown)
  22.  
  23. let filterInput = null;
  24. let formatButton = null;
  25. let inputWrapper = null;
  26. let resizeObserver = null; // To observe wrapper/input size changes
  27.  
  28. function createFormatButton() {
  29. const existingButton = document.getElementById(BUTTON_ID);
  30. if (existingButton) {
  31. return existingButton;
  32. }
  33.  
  34. const button = document.createElement('button');
  35. button.id = BUTTON_ID;
  36. button.textContent = BUTTON_TEXT;
  37. button.type = 'button';
  38.  
  39. button.style.position = 'absolute';
  40. button.style.top = '50%';
  41. button.style.right = '25px'; // Gap from the right edge of the wrapper
  42. button.style.transform = 'translateY(-50%)';
  43. button.style.zIndex = '10';
  44. button.style.padding = '2px 8px';
  45. button.style.fontSize = '12px';
  46. button.style.height = 'calc(100% - 8px)'; // Fit nicely, allowing 4px top/bottom margin in wrapper
  47. button.style.maxHeight = '26px'; // Max height
  48. button.style.lineHeight = '1';
  49. button.style.display = 'inline-flex';
  50. button.style.alignItems = 'center';
  51. button.style.justifyContent = 'center';
  52. button.style.border = '1px solid var(--color-btn-border, #606771)';
  53. button.style.borderRadius = '4px';
  54. button.style.backgroundColor = 'var(--color-btn-bg, #f6f8fa)';
  55. button.style.color = 'var(--color-btn-text, #24292f)';
  56. button.style.cursor = 'pointer';
  57. button.style.fontWeight = '500';
  58. button.style.whiteSpace = 'nowrap'; // Prevent text wrapping in button
  59.  
  60. button.addEventListener('mouseenter', () => {
  61. button.style.backgroundColor = 'var(--color-btn-hover-bg, #f3f4f6)';
  62. button.style.borderColor = 'var(--color-btn-hover-border, #505761)';
  63. });
  64. button.addEventListener('mouseleave', () => {
  65. button.style.backgroundColor = 'var(--color-btn-bg, #f6f8fa)';
  66. button.style.borderColor = 'var(--color-btn-border, #606771)';
  67. });
  68.  
  69. button.addEventListener('click', (event) => {
  70. event.preventDefault();
  71. event.stopPropagation();
  72. const currentDomInput = document.getElementById(INPUT_ID);
  73. if (!currentDomInput) return;
  74. filterInput = currentDomInput;
  75.  
  76. if (filterInput.value.trim() !== '') {
  77. const originalValue = filterInput.value;
  78. const newValue = `title:*${originalValue}*`;
  79. const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set ||
  80. Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
  81. if (valueSetter) {
  82. valueSetter.call(filterInput, newValue);
  83. } else {
  84. filterInput.value = newValue;
  85. }
  86. const inputEvent = new Event('input', { bubbles: true, cancelable: true });
  87. filterInput.dispatchEvent(inputEvent);
  88. filterInput.focus();
  89. }
  90. });
  91. return button;
  92. }
  93.  
  94. function adjustLayout() {
  95. if (!filterInput || !inputWrapper || !formatButton) return;
  96.  
  97. // Ensure input fills the wrapper and has box-sizing: border-box
  98. filterInput.style.width = '100%';
  99. filterInput.style.boxSizing = 'border-box';
  100.  
  101. // Temporarily show button to measure its width if it was hidden
  102. const wasButtonHidden = formatButton.style.display === 'none';
  103. if (wasButtonHidden) {
  104. formatButton.style.visibility = 'hidden'; // Keep it hidden but allow measurement
  105. formatButton.style.display = 'inline-flex';
  106. }
  107.  
  108. const buttonActualWidth = formatButton.offsetWidth;
  109.  
  110. if (wasButtonHidden) { // Restore original hidden state if needed
  111. formatButton.style.display = 'none';
  112. formatButton.style.visibility = 'visible';
  113. }
  114.  
  115. const wrapperInnerWidth = inputWrapper.clientWidth -
  116. (parseFloat(getComputedStyle(inputWrapper).paddingLeft) || 0) -
  117. (parseFloat(getComputedStyle(inputWrapper).paddingRight) || 0);
  118.  
  119. const spaceForButtonAndGap = buttonActualWidth + 10; // 5px on each side of button within input padding
  120.  
  121. // Check if there's enough space for the button AND a minimum text area
  122. if (buttonActualWidth > 0 && (wrapperInnerWidth - spaceForButtonAndGap) >= MIN_INPUT_TEXT_AREA_WIDTH) {
  123. filterInput.style.paddingRight = `${spaceForButtonAndGap}px`;
  124. formatButton.style.display = 'inline-flex'; // Show button
  125. } else {
  126. // Not enough space, hide button and reset padding
  127. filterInput.style.paddingRight = '8px'; // Or original padding if you stored it
  128. formatButton.style.display = 'none'; // Hide button
  129. }
  130. }
  131.  
  132.  
  133. function setupInputAndButton() {
  134. const currentDomInput = document.getElementById(INPUT_ID);
  135.  
  136. if (!currentDomInput) { // Input disappeared, cleanup
  137. if (resizeObserver && inputWrapper) {
  138. resizeObserver.unobserve(inputWrapper);
  139. }
  140. if (formatButton && formatButton.parentNode) formatButton.parentNode.removeChild(formatButton);
  141. if (inputWrapper && inputWrapper.parentNode && inputWrapper.classList.contains(WRAPPER_CLASS)) {
  142. const originalParent = inputWrapper.parentNode;
  143. if (filterInput && document.body.contains(filterInput) && filterInput.parentElement !== originalParent) {
  144. originalParent.insertBefore(filterInput, inputWrapper); // Restore original input
  145. filterInput.style.paddingRight = ''; // Reset padding
  146. filterInput.style.width = ''; // Reset width
  147. }
  148. originalParent.removeChild(inputWrapper);
  149. }
  150. filterInput = null; inputWrapper = null; formatButton = null;
  151. return;
  152. }
  153.  
  154. filterInput = currentDomInput;
  155.  
  156. if (filterInput.parentElement && filterInput.parentElement.classList.contains(WRAPPER_CLASS)) {
  157. inputWrapper = filterInput.parentElement;
  158. } else {
  159. inputWrapper = document.createElement('div');
  160. inputWrapper.classList.add(WRAPPER_CLASS);
  161. inputWrapper.style.position = 'relative'; // For absolute positioned button
  162.  
  163. // Mimic original input's display and allow it to take available space
  164. const originalInputComputedStyle = getComputedStyle(filterInput);
  165. inputWrapper.style.display = originalInputComputedStyle.display;
  166. // If the input was block or flex, it likely took full width of its context
  167. if (originalInputComputedStyle.display === 'block' || originalInputComputedStyle.display === 'flex') {
  168. inputWrapper.style.width = '100%'; // Or copy originalInputComputedStyle.width if it was specific
  169. } else if (originalInputComputedStyle.display === 'inline-block') {
  170. // For inline-block, width might be content-based or explicitly set.
  171. // We let it be content-based unless original input had a specific width.
  172. if (originalInputComputedStyle.width !== 'auto' && !originalInputComputedStyle.width.includes('%')) {
  173. inputWrapper.style.width = originalInputComputedStyle.width;
  174. }
  175. }
  176. // Handle case where input might be directly in a flex container
  177. if (filterInput.parentElement && getComputedStyle(filterInput.parentElement).display.includes('flex')) {
  178. inputWrapper.style.flexGrow = originalInputComputedStyle.flexGrow;
  179. inputWrapper.style.flexShrink = originalInputComputedStyle.flexShrink;
  180. inputWrapper.style.flexBasis = originalInputComputedStyle.flexBasis;
  181. }
  182.  
  183.  
  184. if (filterInput.parentNode) {
  185. filterInput.parentNode.insertBefore(inputWrapper, filterInput);
  186. inputWrapper.appendChild(filterInput);
  187. } else {
  188. console.warn("[Tampermonkey] Filter input has no parent, cannot wrap.");
  189. return;
  190. }
  191. }
  192.  
  193. if (!formatButton || !inputWrapper.contains(formatButton)) {
  194. formatButton = createFormatButton();
  195. inputWrapper.appendChild(formatButton);
  196. }
  197.  
  198. // Initial layout adjustment
  199. adjustLayout();
  200.  
  201. // Observe wrapper for size changes to re-adjust layout
  202. if (typeof ResizeObserver !== 'undefined') {
  203. if (resizeObserver) { // Disconnect from old wrapper if any
  204. resizeObserver.disconnect();
  205. }
  206. resizeObserver = new ResizeObserver(entries => {
  207. // We are observing the wrapper, so direct call to adjustLayout
  208. requestAnimationFrame(adjustLayout); // Debounce with requestAnimationFrame
  209. });
  210. if (inputWrapper) {
  211. resizeObserver.observe(inputWrapper);
  212. }
  213. } else {
  214. // Fallback for browsers without ResizeObserver (less ideal)
  215. // window.addEventListener('resize', adjustLayout); // This is too broad
  216. // console.warn("[Tampermonkey] ResizeObserver not supported. Layout might not be perfectly responsive to container changes.");
  217. }
  218. }
  219.  
  220. const mutationObserver = new MutationObserver(() => {
  221. const currentInput = document.getElementById(INPUT_ID);
  222. if (currentInput) {
  223. if (!inputWrapper || !inputWrapper.contains(currentInput) || !formatButton || !inputWrapper.contains(formatButton)) {
  224. setupInputAndButton();
  225. }
  226. } else if (filterInput) { // Input was there but now it's gone
  227. setupInputAndButton(); // This will trigger the cleanup logic
  228. }
  229. });
  230.  
  231. mutationObserver.observe(document.body, { childList: true, subtree: true });
  232.  
  233. setTimeout(setupInputAndButton, 1000);
  234.  
  235. })();