Skip to content

Instantly share code, notes, and snippets.

@fauxparse
Created June 5, 2025 04:11
Show Gist options
  • Save fauxparse/6d60305328587a17b535ccc092b61dc6 to your computer and use it in GitHub Desktop.
Save fauxparse/6d60305328587a17b535ccc092b61dc6 to your computer and use it in GitHub Desktop.
Add copy button to Github PRs
{
"manifest_version": 2,
"name": "Copy PR details",
"version": "1.0",
"description": "Copies Github PR details to clipboard for pasting into Slack",
"permissions": [
"activeTab"
],
"content_scripts": [
{
"matches": ["*://*.github.com/*"],
"js": ["script.js"]
}
]
}
function addCopyButton() {
const titleElement = document.querySelector('.js-issue-title');
// Check if titleElement exists and if our button hasn't been added yet
if (titleElement && !document.getElementById('copy-pr-details-button')) {
const button = document.createElement('button');
const clipboardIconSVG = '<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-copy"> <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>';
button.id = 'copy-pr-details-button';
button.innerHTML = clipboardIconSVG;
button.type = 'button'; // Good practice for buttons
// Use GitHub's button classes for styling and add a small margin
button.className = 'btn btn-sm';
button.style.marginLeft = '8px';
button.style.marginTop = '-6px';
button.style.cursor = 'pointer';
// Ensure it's vertically aligned reasonably if possible
button.style.verticalAlign = 'middle';
button.addEventListener('click', (event) => {
event.stopPropagation(); // Prevent any other click listeners on parent elements
// Re-fetch the title element at click time to ensure it's still valid,
// especially relevant if the DOM could have changed.
const currentTitleElement = document.querySelector('.js-issue-title');
if (!currentTitleElement) {
console.error('Copy PR Details: Could not find PR title element at click time.');
alert('Copy PR Details: Could not find PR title element.');
return;
}
const issueTitle = currentTitleElement.textContent.trim();
const pageUrl = window.location.href;
const textToCopy = `🔍 [${issueTitle}](${pageUrl})`;
navigator.clipboard.writeText(textToCopy)
.then(() => {
console.log('PR details copied to clipboard:', textToCopy);
button.textContent = 'Copied!';
button.disabled = true;
setTimeout(() => {
button.innerHTML = clipboardIconSVG;
button.disabled = false;
}, 2000);
})
.catch(err => {
console.error('Failed to copy PR details: ', err);
alert('Failed to copy PR details: ' + err.message);
});
});
// Insert the button. Appending to the parent of .js-issue-title is usually a good spot.
if (titleElement.parentElement) {
// Check if the parent is displayed, sometimes titleElement might be in a hidden structure initially
if (window.getComputedStyle(titleElement.parentElement).display !== 'none') {
titleElement.parentElement.appendChild(button);
} else {
// If parent is not visible, try to insert after titleElement itself as a fallback,
// though this might indicate a more complex scenario.
titleElement.insertAdjacentElement('afterend', button);
}
} else {
// Fallback if parentElement is null for some reason
titleElement.insertAdjacentElement('afterend', button);
}
}
}
// --- MutationObserver to handle dynamic content loading (e.g., PJAX navigation in GitHub) ---
const observerCallback = (mutationsList, observer) => {
// Check if the title element is present and the button is not.
// This is a common pattern for SPAs where content is replaced.
if (document.querySelector('.js-issue-title') && !document.getElementById('copy-pr-details-button')) {
addCopyButton();
}
// If the title element is gone, and our button might still be somewhere else (orphaned),
// this doesn't explicitly clean it up, but addCopyButton checks for existence.
};
const observer = new MutationObserver(observerCallback);
function initializeExtension() {
// Call addCopyButton once at the start in case the page is already loaded with the title
addCopyButton();
// Then, observe for changes in the DOM
observer.observe(document.body, { childList: true, subtree: true });
}
// Start the process depending on the document loading state.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeExtension);
} else {
// DOM is already loaded
initializeExtension();
}
// Optional: Disconnect observer on page unload (though browser usually handles this for content scripts)
// window.addEventListener('unload', () => {
// if (observer) {
// observer.disconnect();
// console.log('Copy PR Details: Observer disconnected.');
// }
// });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment