Confluence Jira Title Copy

Add buttons to copy title and link of a Confluence/Jira page, and to copy as filename

// ==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==
// ==/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);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址