// ==UserScript== // @name Confluence Jira Title Copy // @name:zh-CN Confluence Jira 复制标题和链接 // @name:ja Confluence Jira タイトルコピー // @description:zh-CN 点击按钮以markdown格式复制标题文本+链接,以及复制为文件名 // @description:ja ボタンをクリックして、タイトルテキスト+リンクをマークダウン形式でコピーし、ファイル名としてコピーします。 // @namespace http://tampermonkey.net/ // @version 0.8 // @description Add buttons to copy title and link of a Confluence/Jira page, and to copy as filename // @author cheerchen37 // @license MIT // @copyright 2024, https://github.com/cheerchen37/confluence-kopipe // @match *://*.atlassian.net/* // @grant none // @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pjxzdmcgdmlld0JveD0iMCAwIDQwIDQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZS8+PGcgaWQ9IkNvbmZsdWVuY2UiPjxwYXRoIGQ9Ik0yNS42LDIyYzEuODYuOSwzLjkyLDEuODcsNSwyLjM2YS43Ni43NiwwLDAsMSwuMzgsMWwtMi4zNiw1LjM0djBhLjc3Ljc3LDAsMCwxLTEsLjM2bC00LjkyLTIuMzRjLTMuNTctMS43MS01LjU1LTIuMS03LjUxLDEuMTRsLS43NCwxLjIzaDBhLjc2Ljc2LDAsMCwxLTEuMDUuMjVsLTUtMy4wNmEuNzYuNzYsMCwwLDEtLjI1LTFsLjc2LTEuMjVDMTMuMjIsMTksMTguOTEsMTguNzksMjUuNiwyMlptNS41My04LjFjLjI1LS40LjUzLS44Ny43Ni0xLjI1YS43Ni43NiwwLDAsMC0uMjUtMWwtNS0zLjA1YS43Ni43NiwwLDAsMC0xLjA2LjIxbDAsMGMtLjE5LjMzLS40NS43Ny0uNzMsMS4yMy0yLDMuMjQtMy45NCwyLjg1LTcuNTEsMS4xNEwxMi40Myw4Ljg5YS43NS43NSwwLDAsMC0xLC4zN3YwTDksMTQuNjJhLjc4Ljc4LDAsMCwwLC4zOCwxYzEsLjQ5LDMuMTEsMS40Niw1LDIuMzZDMjEuMDksMjEuMjMsMjYuNzgsMjEsMzEuMTMsMTMuOTRaIi8+PC9nPjwvc3ZnPg== // @downloadURL https://update.greasyfork.cloud/scripts/512684/Confluence%20Jira%20Title%20Copy.user.js // @updateURL https://update.greasyfork.cloud/scripts/512684/Confluence%20Jira%20Title%20Copy.meta.js // ==/UserScript== (function() { 'use strict'; // Initialize script function initScript() { // Add button styles const style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = ` .custom-copy-button { display: inline-flex; align-items: center; margin-left: 10px; padding: 6px 12px; background-color: #0052CC; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; vertical-align: middle; } .custom-copy-button:hover { background-color: #003380; } #copyFeedback { position: absolute; margin-top: 5px; background-color: #000; color: #fff; padding: 5px 10px; border-radius: 5px; display: none; z-index: 1001; } `; document.head.appendChild(style); // Initial check for buttons addButtons(); // Set up a timer to check for the elements periodically setInterval(addButtons, 2000); } // Get Confluence page title function getConfluenceTitle() { const selectors = [ '#title-text', 'h1.css-1xrg2ua', 'h1[data-test-id="content-title"]', 'h1.PageTitle', '.confluence-page-title', '.aui-page-header-main h1', '#content-header-container h1', // Additional Confluence selectors '.css-1mpsox7 h1', // New Confluence cloud '#main-content h1:first-child', '.wiki-content .confluenceTitle', 'h1.pagetitle' ]; for (const selector of selectors) { const element = document.querySelector(selector); if (element && element.textContent.trim()) { return element.textContent.trim(); } } return document.title.split(' - ')[0].trim(); } // Get Jira page title function getJiraTitle() { const mainSelector = 'h1[data-testid="issue.views.issue-base.foundation.summary.heading"]'; const mainElement = document.querySelector(mainSelector); if (mainElement && mainElement.textContent.trim()) { return mainElement.textContent.trim(); } const backupSelectors = [ 'h1[data-test-id="issue-title"]', 'h1.issue-title', 'h1.ghx-summary', '.issue-header h1', '.jira-issue-header h1', // Additional selectors for backlog view '.ghx-issue-title', '[data-testid="rapid-board-issue.ui.issue-card.title-container"]', '[role="heading"][aria-level="3"]' // Often used in backlog ]; for (const selector of backupSelectors) { const elements = document.querySelectorAll(selector); if (elements && elements.length > 0) { // Return the one that's visible or the first one for (const element of elements) { if (element && element.textContent.trim() && isElementVisible(element)) { return element.textContent.trim(); } } // If no visible element found, return the first one if (elements[0].textContent.trim()) { return elements[0].textContent.trim(); } } } return document.title.split(' - ')[0].trim(); } // Check if element is visible function isElementVisible(element) { return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); } // Get Jira ticket ID from URL or page function getJiraTicketId() { // First try to extract ticket ID from selectedIssue parameter in backlog URL if (window.location.href.includes('/backlog') && window.location.href.includes('selectedIssue=')) { const urlParams = new URLSearchParams(window.location.search); const selectedIssue = urlParams.get('selectedIssue'); if (selectedIssue) { return selectedIssue; } } // Try to extract ticket ID from /browse/ URL const urlMatch = window.location.href.match(/\/browse\/([A-Z]+-\d+)/i); if (urlMatch && urlMatch[1]) { return urlMatch[1]; } return ''; } // Sanitize string for use as filename function sanitizeFilename(input) { return input.replace(/[\\/:*?"<>|[\]{}#%&+,;=@^`~]/g, '-') .replace(/\s+/g, ' ') .trim(); } // Add copy buttons to page function addButtons() { // Check if we're in Jira if (document.location.href.includes("/browse/") || document.location.href.includes("/jira/") || document.location.href.includes("/backlog")) { addJiraButtons(); } // Check if we're in Confluence else if (document.location.href.includes("/wiki/") || document.location.href.includes("/confluence/") || document.location.href.includes("/display/")) { addConfluenceButtons(); } } // Add buttons to Jira pages function addJiraButtons() { if (document.getElementById('customCopyButton')) { return; // Buttons already exist } let titleElement = null; // Try to find the title element using various selectors const titleSelectors = [ 'h1[data-testid="issue.views.issue-base.foundation.summary.heading"]', '.issue-header h1', '.jira-issue-header h1', '.ghx-detail-title h1', // For backlog view '[data-testid="rapid-board-issue.ui.issue-card.title-container"]', '.ghx-selected .ghx-summary' ]; for (const selector of titleSelectors) { const element = document.querySelector(selector); if (element && isElementVisible(element)) { titleElement = element; break; } } // If we found a title element, add buttons next to it if (titleElement) { insertButtonsNextToElement(titleElement); } else { // For backlog, try to add to a visible container const backlogContainers = [ '[data-test-id="platform-board.ui.board.board-container"]', '.ghx-detail-view', '.ghx-detail-contents', '[data-testid="software-board.board.board.container"]' ]; for (const selector of backlogContainers) { const container = document.querySelector(selector); if (container && isElementVisible(container)) { // Create floating buttons for backlog insertFloatingButtons(container); break; } } } } // Add buttons to Confluence pages function addConfluenceButtons() { if (document.getElementById('customCopyButton')) { return; // Buttons already exist } let titleElement = null; // Try to find the confluence title using various selectors const titleSelectors = [ '#title-text', 'h1.css-1xrg2ua', 'h1[data-test-id="content-title"]', 'h1.PageTitle', '.confluence-page-title', '.aui-page-header-main h1', '#content-header-container h1', // Additional Confluence selectors '.css-1mpsox7 h1', '#main-content h1:first-child', '.wiki-content .confluenceTitle', 'h1.pagetitle', // Generic h1 as last resort '#main-content h1' ]; for (const selector of titleSelectors) { const element = document.querySelector(selector); if (element && isElementVisible(element)) { titleElement = element; break; } } // If we found a title element, add buttons next to it if (titleElement) { insertButtonsNextToElement(titleElement); } else { // Try adding to a container const containers = [ '#main-content', '.confluence-information-macro-body', '.wiki-content', '.content-body', '#content' ]; for (const selector of containers) { const container = document.querySelector(selector); if (container && isElementVisible(container)) { insertFloatingButtons(container); break; } } } } // Insert buttons next to an element function insertButtonsNextToElement(element) { // Create button container const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'inline-flex'; buttonContainer.style.alignItems = 'center'; buttonContainer.style.marginLeft = '10px'; // Create markdown button const button = document.createElement('button'); button.id = 'customCopyButton'; button.className = 'custom-copy-button'; button.textContent = 'Copy Title & Link'; buttonContainer.appendChild(button); // Create filename button const filenameButton = document.createElement('button'); filenameButton.id = 'customCopyFilenameButton'; filenameButton.className = 'custom-copy-button'; filenameButton.style.marginLeft = '5px'; filenameButton.textContent = 'Copy as Filename'; buttonContainer.appendChild(filenameButton); // Create feedback element const feedback = document.createElement('div'); feedback.id = 'copyFeedback'; feedback.textContent = 'Copied!'; buttonContainer.appendChild(feedback); // Insert container after the element if (element.nextSibling) { element.parentNode.insertBefore(buttonContainer, element.nextSibling); } else { element.parentNode.appendChild(buttonContainer); } // Add event listeners button.addEventListener('click', copyAsMarkdown); filenameButton.addEventListener('click', copyAsFilename); } // Insert floating buttons in a container function insertFloatingButtons(container) { // Create floating button container const buttonContainer = document.createElement('div'); buttonContainer.style.position = 'absolute'; buttonContainer.style.top = '10px'; buttonContainer.style.right = '10px'; buttonContainer.style.zIndex = '1000'; buttonContainer.style.display = 'flex'; // Create markdown button const button = document.createElement('button'); button.id = 'customCopyButton'; button.className = 'custom-copy-button'; button.textContent = 'Copy Title & Link'; buttonContainer.appendChild(button); // Create filename button const filenameButton = document.createElement('button'); filenameButton.id = 'customCopyFilenameButton'; filenameButton.className = 'custom-copy-button'; filenameButton.style.marginLeft = '5px'; filenameButton.textContent = 'Copy as Filename'; buttonContainer.appendChild(filenameButton); // Create feedback element const feedback = document.createElement('div'); feedback.id = 'copyFeedback'; feedback.textContent = 'Copied!'; feedback.style.position = 'absolute'; feedback.style.top = '40px'; feedback.style.right = '0'; buttonContainer.appendChild(feedback); // Make sure container has position relative const currentPosition = window.getComputedStyle(container).position; if (currentPosition === 'static') { container.style.position = 'relative'; } // Add to container container.appendChild(buttonContainer); // Add event listeners button.addEventListener('click', copyAsMarkdown); filenameButton.addEventListener('click', copyAsFilename); } // Copy title and link as Markdown function copyAsMarkdown() { let titleText = ''; if (document.location.href.includes("wiki") || document.location.href.includes("confluence")) { titleText = getConfluenceTitle(); console.log("Got Confluence title:", titleText); } else { titleText = getJiraTitle(); console.log("Got Jira title:", titleText); } if (!titleText) { titleText = document.title; console.log("Using document title:", titleText); } const pageLink = window.location.href; const copyText = `[${titleText}](${pageLink})`; console.log("Copying as Markdown:", copyText); copyToClipboard(copyText); } // Copy as filename format function copyAsFilename() { let titleText = ''; let ticketId = ''; if (document.location.href.includes("wiki") || document.location.href.includes("confluence")) { titleText = getConfluenceTitle(); } else { titleText = getJiraTitle(); ticketId = getJiraTicketId(); console.log("Extracted ticket ID:", ticketId); } if (!titleText) { titleText = document.title; } // Sanitize title to be safe for filenames titleText = sanitizeFilename(titleText); // Create filename format: {ticket-id} {title} let filename = titleText; if (ticketId) { filename = `${ticketId} ${titleText}`; } console.log("Copying as filename:", filename); copyToClipboard(filename); } // Copy text to clipboard function copyToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => { showFeedback(); }).catch(err => { console.error("Clipboard API failed:", err); copyWithFallback(text); }); } else { copyWithFallback(text); } } // Fallback copy method function copyWithFallback(text) { const tempTextarea = document.createElement('textarea'); tempTextarea.style.position = 'fixed'; tempTextarea.style.top = '0'; tempTextarea.style.left = '0'; tempTextarea.style.width = '2em'; tempTextarea.style.height = '2em'; tempTextarea.style.opacity = '0'; document.body.appendChild(tempTextarea); tempTextarea.value = text; tempTextarea.select(); try { const success = document.execCommand('copy'); if (success) { showFeedback(); } else { console.error("execCommand copy failed"); } } catch (err) { console.error('Copy error:', err); } document.body.removeChild(tempTextarea); } // Show copy success feedback function showFeedback() { const feedback = document.getElementById('copyFeedback'); if (feedback) { feedback.style.display = 'block'; setTimeout(() => { feedback.style.display = 'none'; }, 2000); } } // Initialize on page load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(initScript, 500)); } else { setTimeout(initScript, 500); } // Watch for DOM changes const observer = new MutationObserver(function(mutations) { if (!document.getElementById('customCopyButton')) { addButtons(); } }); // Start observing after initialization setTimeout(() => { observer.observe(document.body, { childList: true, subtree: true }); }, 1000); })();