Confluence Jira Title Copy

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

2025-03-27 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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